/* ══════════════════════════════════════════════════════════════════
   AUGENSCHEIN V&L — React-App (Stufe 1)
   Portierung des HTML-Prototyps nach React mit Babel-Standalone.
   Visuell und funktional identisch zum Ausgangsmaterial.

   Struktur:
   1. Icons (wiederverwendbare SVG-Komponenten)
   2. Beispieldaten (3 Projekte als Konstante)
   3. UI-Bausteine (Pill, Card, ProgressRow, etc.)
   4. Views (Projektliste, Dashboard, Auftrag, Gutachten)
   5. Top-Level App mit zentralem State und Navigation
   6. Mount via ReactDOM
════════════════════════════════════════════════════════════════════ */

const { useState, useEffect, useCallback, useMemo, useRef, Fragment } = React;

// ══════════════════════════════════════════════════════════════════
// 1 · ICONS
// ══════════════════════════════════════════════════════════════════
const Icon = ({ path, size = 16, stroke = "currentColor", strokeWidth = 2, className = "" }) => (
  <svg className={`icon icon-${size} ${className}`} width={size} height={size}
       viewBox="0 0 24 24" fill="none" stroke={stroke} strokeWidth={strokeWidth}
       strokeLinecap="round" strokeLinejoin="round">
    {path}
  </svg>
);

const IconChevronLeft = (p) => <Icon {...p} path={<path d="M15 18l-6-6 6-6"/>} />;
const IconChevronRight = (p) => <Icon {...p} path={<path d="M9 18l6-6-6-6"/>} />;
const IconChevronDown = (p) => <Icon {...p} path={<path d="M6 9l6 6 6-6"/>} />;
const IconDocument = (p) => <Icon {...p} path={<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></>} />;
const IconUpload = (p) => <Icon {...p} path={<><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></>} strokeWidth={1.5} />;
const IconExternalLink = (p) => <Icon {...p} path={<><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></>} strokeWidth={2} />;
const IconProvenance = (p) => <Icon {...p} path={<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="15" x2="15" y2="15"/></>} strokeWidth={1.8} />;
const IconMic = (p) => <Icon {...p} path={<><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10v2a7 7 0 0 0 14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></>} strokeWidth={2} />;
const IconRoute = (p) => <Icon {...p} path={<><circle cx="6" cy="19" r="3"/><path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"/><circle cx="18" cy="5" r="3"/></>} strokeWidth={2} />;
const IconCamera = (p) => <Icon {...p} path={<><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></>} strokeWidth={2} />;
const IconClose = (p) => <Icon {...p} path={<><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></>} strokeWidth={2} />;
const IconStop = (p) => <Icon {...p} path={<rect x="5" y="5" width="14" height="14" rx="2"/>} strokeWidth={2} />;
const IconVolume = (p) => <Icon {...p} path={<><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></>} strokeWidth={2} />;
const IconCheck = (p) => <Icon {...p} path={<polyline points="20 6 9 17 4 12"/>} strokeWidth={2.5} />;
const IconEye = (p) => <Icon {...p} path={<><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></>} strokeWidth={2} />;
const IconClock = (p) => <Icon {...p} path={<><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></>} strokeWidth={2} />;

// ══════════════════════════════════════════════════════════════════
// 2 · DATA-LAYER (Supabase)
// Ersetzt die hartcodierten PROJEKTE durch echte DB-Queries.
//
// Strategie:
//   - Worker liefert unter /api/config die Supabase-Credentials
//   - Supabase-JS wird per <script> im HTML geladen (globale `supabase`-Lib)
//   - Adapter transformiert DB-Rows in das gleiche Datenobjekt-Format
//     wie früher die PROJEKTE-Konstante, damit die Komponenten unverändert
//     bleiben
// ══════════════════════════════════════════════════════════════════

// Worker-URL — zeigt in Dev auf lokalen Worker, in Prod auf Cloudflare
const WORKER_URL =
  window.location.hostname === 'sv.augenschein.app'      ? 'https://api-sv.augenschein.app'
  : window.location.hostname === 'custom.augenschein.app' ? 'https://api-custom.augenschein.app'
  : 'https://api-custom.augenschein.app'; // Dev: Prod-Worker

// Produkt-Modus per Hostname: sv = Bauschaden-Produkt, sonst Verkehrswert (custom/dev).
// Alle bauschaden-spezifischen Abzweige sind hieran gegated → custom bleibt unverändert.
const APP_MODE = window.location.hostname === 'sv.augenschein.app' ? 'bauschaden' : 'verkehrswert';

// ────────────────────────────────────────────────────────────────────
// SESSION ENFORCEMENT — "Max 2 Geräte"
//
// Verhindert, dass mehr als zwei Geräte denselben Account gleichzeitig nutzen.
// Funktionsweise:
//   1. Beim Login generiert der Client eine sessionId (UUID) und speichert
//      sie in localStorage + ruft POST /api/session/claim auf.
//   2. Jeder authentifizierte Fetch bekommt automatisch den Header
//      X-Session-Id injiziert (via globalem Fetch-Wrapper).
//   3. Der Worker prüft ob die X-Session-Id in der sessions-Tabelle
//      existiert. Pro User sind max. 2 Sessions erlaubt.
//      Bei einem dritten Login wird die älteste Session entfernt.
//   4. Der Fetch-Wrapper fängt 409 ab und dispatcht ein Event,
//      das die App als Overlay anzeigt. Der User kann dort
//      "Hier fortfahren" wählen (re-claim, verdrängt älteste)
//      oder sich abmelden.
// ────────────────────────────────────────────────────────────────────
const SESSION_ID_KEY = 'augenschein_session_id';
let _activeSessionId = localStorage.getItem(SESSION_ID_KEY) || null;

function generateSessionId() {
  const id = crypto.randomUUID ? crypto.randomUUID()
    : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,c=>(c^crypto.getRandomValues(new Uint8Array(1))[0]&15>>c/4).toString(16));
  localStorage.setItem(SESSION_ID_KEY, id);
  _activeSessionId = id;
  return id;
}

function clearSessionId() {
  localStorage.removeItem(SESSION_ID_KEY);
  _activeSessionId = null;
}

async function claimSession(accessToken) {
  if (!_activeSessionId) return;
  try {
    await _originalFetch(`${WORKER_URL}/api/session/claim`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}`,
      },
      body: JSON.stringify({ session_id: _activeSessionId }),
    });
  } catch (e) {
    console.warn('[SessionEnforcement] Claim failed:', e.message);
  }
}

// Fetch-Wrapper: Injiziert X-Session-Id Header und fängt 409 ab
const _originalFetch = window.fetch.bind(window);
window.fetch = async function(url, options = {}) {
  // X-Session-Id nur injizieren wenn authentifizierter Request
  if (_activeSessionId && options.headers) {
    const h = options.headers;
    const hasAuth = (typeof h === 'object' && !Array.isArray(h))
      ? (h['Authorization'] || h['authorization'])
      : false;
    if (hasAuth && typeof h === 'object') {
      h['X-Session-Id'] = _activeSessionId;
    }
  }

  const response = await _originalFetch(url, options);

  // 409 Session Superseded abfangen
  if (response.status === 409) {
    try {
      const clone = response.clone();
      const body = await clone.json();
      if (body.error === 'session_superseded') {
        window.dispatchEvent(new CustomEvent('session-superseded'));
      }
    } catch {}
  }

  return response;
};

// Supabase-Client wird nach Config-Fetch initialisiert
let supabaseClient = null;

async function initSupabase() {
  if (supabaseClient) return supabaseClient;

  const res = await fetch(`${WORKER_URL}/api/config`);
  if (!res.ok) throw new Error('Worker /api/config nicht erreichbar');
  const cfg = await res.json();

  // window.supabase kommt aus dem CDN-Script in vl-index.html
  if (!window.supabase) throw new Error('Supabase-JS nicht geladen (window.supabase fehlt)');

  supabaseClient = window.supabase.createClient(cfg.supabase_url, cfg.supabase_anon_key, {
    auth: { persistSession: true, autoRefreshToken: true },
  });

  return supabaseClient;
}

// ────────────────────────────────────────────────────────────────────
// Registry-Loader
// Holt die Dokumenttyp-Registry einmal vom Worker (GET /api/registry)
// und cached das Ergebnis modul-weit. Der Worker ist Single Source of
// Truth für Dokumenttypen (id, label, group, scope, extractable,
// requiredFor, requiredIf). UPLOAD_TYPES bleibt als Fallback, falls
// der Fetch fehlschlägt oder noch nicht zurück ist.
// ────────────────────────────────────────────────────────────────────
let _fetchedRegistry = null;
let _registryPromise = null;

async function loadRegistry() {
  if (_fetchedRegistry) return _fetchedRegistry;
  if (_registryPromise) return _registryPromise;
  _registryPromise = (async () => {
    try {
      const res = await fetch(`${WORKER_URL}/api/registry`);
      if (!res.ok) return null;
      const j = await res.json();
      _fetchedRegistry = j.registry || null;
      return _fetchedRegistry;
    } catch {
      _registryPromise = null;
      return null;
    }
  })();
  return _registryPromise;
}

// ── Tag-Herkunft (Provenance) für die Transparenz-Anzeige im Entwurf ──
// Statische Map (tag_id → { kategorie, label, ... }), einmal geladen + gecacht.
let _fetchedProvenance = null;
let _provenancePromise = null;

async function loadTagProvenance() {
  if (_fetchedProvenance) return _fetchedProvenance;
  if (_provenancePromise) return _provenancePromise;
  _provenancePromise = (async () => {
    try {
      const res = await fetch(`${WORKER_URL}/api/entwurf/tag-provenance`);
      if (!res.ok) return null;
      const j = await res.json();
      _fetchedProvenance = j.provenance || null;
      return _fetchedProvenance;
    } catch {
      _provenancePromise = null;
      return null;
    }
  })();
  return _provenancePromise;
}

// Herkunft eines Tags abrufen (oder null, wenn Map noch nicht geladen / unbekannt)
function tagProvenanceFor(tagId) {
  if (_fetchedProvenance && _fetchedProvenance[tagId]) return _fetchedProvenance[tagId];
  return null;
}

// Prüft, ob ein Dokumenttyp seine Felder in feste Fachtabellen-Spalten routet
// (dann erscheinen sie in den Stammdaten-Sektionen). Quelle: hasRouting-Flag
// aus der Registry; Fallback-Liste, falls die Registry noch nicht geladen ist.
const _ROUTING_TYPEN_FALLBACK = new Set([
  'gerichtsbeschluss', 'anschreiben', 'grundbuchauszug',
  'bebauungsplan', 'baurechtsauskunft', 'bodenrichtwert', 'energieausweis',
]);
function typHatRouting(typId) {
  if (_fetchedRegistry && Array.isArray(_fetchedRegistry)) {
    const hit = _fetchedRegistry.find(t => t.id === typId);
    if (hit && typeof hit.hasRouting === 'boolean') return hit.hasRouting;
  }
  return _ROUTING_TYPEN_FALLBACK.has(typId);
}

// Liefert die vollständige erwartete Feldliste eines Dokumenttyps (aus dem
// Prompt-Schema, via Registry). Damit kann das Frontend auch nicht-extrahierte
// (leere) Felder anzeigen, die der Nutzer manuell aus dem PDF nachtragen kann.
function expectedFieldsFor(typId) {
  if (_fetchedRegistry && Array.isArray(_fetchedRegistry)) {
    const hit = _fetchedRegistry.find(t => t.id === typId);
    if (hit && Array.isArray(hit.expectedFields)) return hit.expectedFields;
  }
  return [];
}

// Wandelt die flache Registry-Liste in das gruppierte UPLOAD_TYPES-Format um.
// Reihenfolge der Gruppen: auftrag → gutachten → objekt (falls abweichend
// vom Server-Default).
function groupRegistryByScope(flat) {
  if (!Array.isArray(flat) || flat.length === 0) return null;
  const byGroup = new Map();
  for (const t of flat) {
    // Wir gruppieren visuell nach dem 'group'-Feld (z.B. "Gerichts­unterlagen"),
    // behalten aber scope pro Gruppe für Scope-Auto-Detection im Upload.
    const key = t.group || t.scope || 'Sonstige';
    if (!byGroup.has(key)) {
      byGroup.set(key, { group: key, scope: t.scope, items: [] });
    }
    byGroup.get(key).items.push({
      id: t.id,
      label: t.label,
      extractable: t.extractable,
      requiredFor: t.requiredFor || null,
      requiredIf: t.requiredIf || null,
    });
  }
  // Reihenfolge stabilisieren: auftrag-Scope zuerst, dann gutachten, dann objekt
  const order = { auftrag: 0, gutachten: 1, objekt: 2 };
  return Array.from(byGroup.values()).sort(
    (a, b) => (order[a.scope] ?? 99) - (order[b.scope] ?? 99)
  );
}

// Bestimmt, ob ein Dokumenttyp für einen gegebenen Kontext Pflicht ist.
// auftragsart: 'gericht' | 'privat' | ... (raw-Enum aus auftraege.auftragsart)
// objekttypen: Array der vorkommenden Objekttypen im Gutachten (raw)
// Ein Typ ist Pflicht, wenn entweder requiredFor die Auftragsart enthält
// oder requiredIf auf mindestens einen der vorkommenden Objekttypen passt.
function isTypeRequired(item, auftragsart, objekttypen) {
  if (Array.isArray(item.requiredFor) && item.requiredFor.includes(auftragsart)) return true;
  if (item.requiredIf?.objekttyp && Array.isArray(objekttypen)) {
    return objekttypen.some(ot => item.requiredIf.objekttyp.includes(ot));
  }
  return false;
}

// Hook: gibt die aktuelle UPLOAD_TYPES-Struktur zurück (geladen oder Fallback)
function useUploadTypes() {
  const [types, setTypes] = useState(() => {
    if (_fetchedRegistry) {
      const grouped = groupRegistryByScope(_fetchedRegistry);
      if (grouped) return grouped;
    }
    return null;  // Fallback wird von den Consumern verwendet
  });
  useEffect(() => {
    if (types) return;
    let active = true;
    loadRegistry().then(flat => {
      if (!active || !flat) return;
      const grouped = groupRegistryByScope(flat);
      if (grouped) setTypes(grouped);
    });
    return () => { active = false; };
  }, [types]);
  return types;
}

// ────────────────────────────────────────────────────────────────────
// useOrgMembers — lädt alle Mitglieder der gleichen Organisation
// Gibt Array von { id, name, email, isMe } zurück. Cached nach erstem Load.
// ────────────────────────────────────────────────────────────────────
let _orgMembersCache = null;

// ────────────────────────────────────────────────────────────────────
// useIsMobile — gibt true zurück wenn Viewport < 768px
// ────────────────────────────────────────────────────────────────────
function useIsMobile() {
  const [mobile, setMobile] = useState(() => window.innerWidth < 768);
  useEffect(() => {
    const handler = () => setMobile(window.innerWidth < 768);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return mobile;
}

function useOrgMembers(session, workerUrl) {
  const [members, setMembers] = useState(_orgMembersCache);
  useEffect(() => {
    if (_orgMembersCache || !session?.access_token || !workerUrl) return;
    let active = true;
    (async () => {
      try {
        // Frischen Token holen falls der gespeicherte abgelaufen ist
        let token = session.access_token;
        try {
          const sb = await initSupabase();
          const { data } = await sb.auth.getSession();
          if (data?.session?.access_token) token = data.session.access_token;
        } catch {}
        const res = await fetch(`${workerUrl}/api/org-members`, {
          headers: { Authorization: `Bearer ${token}` },
        });
        if (res.ok) {
          const data = await res.json();
          _orgMembersCache = data;
          if (active) setMembers(data);
        }
      } catch (e) { console.warn('[useOrgMembers] failed:', e); }
    })();
    return () => { active = false; };
  }, [session, workerUrl]);
  return members;
}

// ────────────────────────────────────────────────────────────────────
// useRealtimeProject — Supabase Realtime für Live-Updates
// Wenn ein anderer User Änderungen am aktuellen Projekt macht,
// wird refreshProjekt aufgerufen + optionaler Toast.
// ────────────────────────────────────────────────────────────────────
function useRealtimeProject(projektId, session, onRefresh, onOtherUserChange) {
  const channelRef = useRef(null);

  useEffect(() => {
    if (!projektId || !session?.access_token) return;

    let cancelled = false;

    (async () => {
      const sb = await initSupabase();

      // Einen Channel für alle Tabellen des Projekts
      const channel = sb
        .channel(`project-${projektId}`)
        // projects-Tabelle direkt
        .on('postgres_changes',
          { event: '*', schema: 'public', table: 'projects', filter: `id=eq.${projektId}` },
          (payload) => {
            if (cancelled) return;
            const changedBy = payload.new?.updated_by;
            if (changedBy && changedBy !== session.user.id) {
              onOtherUserChange?.(changedBy, 'projects');
            }
            onRefresh?.();
          }
        )
        // auftraege
        .on('postgres_changes',
          { event: '*', schema: 'public', table: 'auftraege', filter: `project_id=eq.${projektId}` },
          () => { if (!cancelled) onRefresh?.(); }
        )
        // gutachten
        .on('postgres_changes',
          { event: '*', schema: 'public', table: 'gutachten', filter: `project_id=eq.${projektId}` },
          () => { if (!cancelled) onRefresh?.(); }
        )
        // dokumente
        .on('postgres_changes',
          { event: '*', schema: 'public', table: 'dokumente', filter: `project_id=eq.${projektId}` },
          () => { if (!cancelled) onRefresh?.(); }
        )
        // beteiligte
        .on('postgres_changes',
          { event: '*', schema: 'public', table: 'beteiligte', filter: `project_id=eq.${projektId}` },
          () => { if (!cancelled) onRefresh?.(); }
        )
        .subscribe((status) => {
          if (status === 'SUBSCRIBED') {
            console.log('[Realtime] Subscribed to project', projektId);
          }
        });

      channelRef.current = channel;
    })();

    return () => {
      cancelled = true;
      if (channelRef.current) {
        initSupabase().then(sb => {
          sb.removeChannel(channelRef.current);
          channelRef.current = null;
        }).catch(() => {});
      }
    };
  }, [projektId, session]);
}

// ────────────────────────────────────────────────────────────────────
// Adapter: Supabase-Rows → Frontend-Datenobjekt
// Übersetzt das Datenbank-Schema ins Format, das die Komponenten erwarten.
// ────────────────────────────────────────────────────────────────────

function adaptAuftrag(row) {
  if (!row) return {};
  // Fallback-Kette für Auftragsnummer: akte > aktenzeichen > geschaeftszeichen
  const akteValue = row.akte || row.aktenzeichen || row.geschaeftszeichen || '';
  return {
    auftrag_id: row.id || null,
    akte: akteValue,
    bearbeitungsstatus: row.bearbeitungsstatus || null, // Manueller Workflow-Status
    auftragsart: auftragsartLabel(row.auftragsart),
    auftragsart_raw: row.auftragsart,  // 'gericht' | 'privat' | …  für UI-Branching
    auftragstyp: auftragstypLabel(row.auftragstyp),
    verfahren: row.verfahrensart || null,
    verfahrensart: row.verfahrensart || null,
    auftraggeber: row.auftraggeber || '',
    auftragseingang: formatDate(row.auftragseingang),
    beschlussdatum: formatDate(row.beschlussdatum),
    fristIntern: formatDate(row.abgabe_intern),
    fristExtern: formatDate(row.abgabe_extern),
    fristRestDays: row.abgabe_extern ? daysUntil(row.abgabe_extern) : null,
    kostenvorschuss: row.kostenvorschuss ? Number(row.kostenvorschuss) : null,
    vereinbarterStundensatz: row.vereinbarter_stundensatz ? Number(row.vereinbarter_stundensatz) : null,
    ausfertigungen: row.ausfertigungen,
    gerichtstyp: row.gerichtstyp || null,
    sache: row.sache || null,
    wegen: row.wegen || null,
    zweck: row.zweck || null,
    belastungenAbt2: row.belastungen_abt_2 || null,
    auftragsbeschreibung: row.auftragsbeschreibung || null,
    // Bearbeiter
    sachverstaendiger: row.sachverstaendiger || null,
    sachbearbeiter: row.sachbearbeiter || null,
    // Nachgelagerte Schritte
    zuschlag_datum: row.zuschlag_datum || null,
    zuschlag_betrag: row.zuschlag_betrag ? Number(row.zuschlag_betrag) : null,
    widerspruch_1_vom: row.widerspruch_1_vom || null,
    frist_stellungnahme_1: row.frist_stellungnahme_1 || null,
    stellungnahme_1_vom: row.stellungnahme_1_vom || null,
    anhoerung_1_vom: row.anhoerung_1_vom || null,
    // Auftrag-Notizen und separate AZ-Felder
    notizen: row.notizen || null,
    aktenzeichen: row.aktenzeichen || null,
    geschaeftszeichen: row.geschaeftszeichen || null,
  };
}

function adaptBeteiligte(rows) {
  return (rows || []).map(r => ({
    id: r.id,
    rolle: r.rolle,
    name: r.name,
    // Einzelfelder für Inline-Edit im Beteiligten-Panel
    anschrift: r.anschrift || null,
    telefon: r.telefon || null,
    email: r.email || null,
    anwalt_name: r.anwalt_name || null,
    anwalt_anschrift: r.anwalt_anschrift || null,
    aktenzeichen: r.aktenzeichen || null,
    // `detail` als vorformatierter Fallback für kompakte Anzeige
    detail: [r.anschrift, r.telefon ? `Tel. ${r.telefon}` : null, r.email, r.anwalt_name ? `RA: ${r.anwalt_name}` : null, r.aktenzeichen ? `Az. ${r.aktenzeichen}` : null]
      .filter(Boolean).join(' · '),
  }));
}

function adaptObjekt(row) {
  return {
    id: row.id,
    bezeichnung: row.bezeichnung,
    objekttyp: objekttypLabel(row.objekttyp),
    kategorie: row.kategorie || 'hauptgebaeude',
    geschosse: row.geschosse || ['kg', 'eg', 'dg'],
    sort_order: row.sort_order || 0,
    // Grundstück
    flur: row.flur || '',
    flurstueck: row.flurstueck || '',
    gemarkung: row.gemarkung || '',
    groesse: row.groesse_qm ? `${row.groesse_qm} m²` : null,
    groesse_qm: row.groesse_qm || null,
    grundbuchblatt: row.grundbuchblatt || null,
    wirtschaftsart: row.wirtschaftsart || null,
    // Gebäude
    wohnflaeche: row.wohnflaeche || '',
    baujahr: row.baujahr || '',
    // Eigentum (WEG)
    bruchteilseigentum: row.bruchteilseigentum || null,
    mea: row.mea || null,
    sondereigentumseinheit: row.sondereigentumseinheit || null,
    herrschvermerk: row.herrschvermerk || null,
    // ETW (Gruppe C): Freitext-Beschreibung für die Kapitel-1-Zeile
    // "Art und Lage des Bewertungsobjekts" — z.B. "3 Zi.-Whg. im 11. OG …"
    art_lage_bewertungsobjekt: row.art_lage_bewertungsobjekt || null,
    // Grundbuch-Belastungen
    abt_2_eintragungen: row.abt_2_eintragungen || null,
    abt_3_eintragungen: row.abt_3_eintragungen || null,
    // Planungsrecht (B-Plan)
    art_bauliche_nutzung: row.art_bauliche_nutzung || null,
    grz: row.grz || null,
    gfz: row.gfz || null,
    vollgeschosse_zulaessig: row.vollgeschosse_zulaessig || null,
    bauweise: row.bauweise || null,
    dachform: row.dachform || null,
    bplan_nr: row.bplan_nr || null,
    bplan_status: row.bplan_status || null,
    festsetzungen_text: row.festsetzungen_text || null,
    // Bodenrichtwert
    bodenrichtwert: row.bodenrichtwert || null,
    brw_stichtag: row.brw_stichtag || null,
    brw_zone: row.brw_zone || null,
    brw_entwicklungszustand: row.brw_entwicklungszustand || null,
    brw_nutzungsart: row.brw_nutzungsart || null,
    brw_gfz: row.brw_gfz || null,
    // Energieausweis
    endenergie: row.endenergie || null,
    primaerenergie: row.primaerenergie || null,
    effizienzklasse: row.effizienzklasse || null,
    energieausweis_typ: row.energieausweis_typ || null,
    energietraeger: row.energietraeger || null,
    baujahr_heizung: row.baujahr_heizung || null,
    energieausweis_gueltig_bis: row.energieausweis_gueltig_bis || null,
    energieausweis_registriernummer: row.energieausweis_registriernummer || null,
  };
}

function adaptGutachten(gRow, objekteRows) {
  const objekte = (objekteRows || [])
    .filter(o => o.gutachten_id === gRow.id)
    .sort((a, b) => a.sort_order - b.sort_order)
    .map(adaptObjekt);

  return {
    id: gRow.id,
    art: gRow.art || 'verkehrswert',
    gewerk: gRow.gewerk || null,
    beweisfragen: [],
    titel: gRow.titel || '',
    adresse: gRow.adresse || '',
    ortsterminStatus: gRow.ortstermin_status || 'geplant',
    ortstermin_datum: gRow.ortstermin_datum || null,
    ortstermin_uhrzeit: gRow.ortstermin_uhrzeit || null,
    ortstermin_anwesende: gRow.ortstermin_anwesende || null,
    innenbesichtigung: gRow.innenbesichtigung !== false, // Default true (Normalfall)
    unterlagen_hinweis: gRow.unterlagen_hinweis || '', // Manuelle Notiz zu Unterlagen (A1)
    // Gruppe C/D-Punkt 21: Freitext-Beschreibung der Gebäude auf dem Grundstück
    // (z.B. "Wohnanlage, bestehend aus vier MFH und einer Tiefgarage" oder
    // "freistehendes EFH, Garage, Scheune"). Überschreibt die stumpfe bebauungMap.
    vorhandene_bebauung_beschreibung: gRow.vorhandene_bebauung_beschreibung || '',
    wertermittlungsstichtag: gRow.wertermittlungsstichtag || null,
    qualitaetsstichtag: gRow.qualitaetsstichtag || null,
    wertermittlungsmethode: gRow.wertermittlungsmethode || null,
    verkehrswert: gRow.verkehrswert || null,
    bodenwert: gRow.bodenwert || null,
    sachwert: gRow.sachwert || null,
    ausfertigungsdatum: gRow.ausfertigungsdatum || null,
    entwurfPct: gRow.entwurf_pct || 0,
    unterlagenDone: 0,
    unterlagenTotal: 0,
    titelbild_path: gRow.titelbild_path || null,
    foto_zuordnung: gRow.foto_zuordnung || {},
    objekte,
  };
}

function adaptDokumente(rows) {
  return (rows || []).map(d => ({
    id: d.id,
    typ: dokumenttypLabel(d.typ),
    typ_raw: d.typ,
    scope: d.scope,
    status: d.extract_status === 'done' ? 'done'
          : d.extract_status === 'angefragt' ? 'angefragt'
          : 'fehlt',
    label: d.titel || '(unbenannt)',
    gutachten_id: d.gutachten_id || null,
    objekt_id: d.objekt_id || null,
    file_path: d.file_path || null,
    mime_type: d.mime_type || null,
    extracted_fields: d.extracted_fields || null,
    // Fristen-Tracking
    angefragt_am: d.angefragt_am || null,
    eingetroffen_am: d.eingetroffen_am || null,
    frist_bis: d.frist_bis || null,
  }));
}

function adaptProjekt(projRow, auftragRow, beteiligteRows, gutachtenRows, objekteRows, dokumenteRows) {
  const gutachten = (gutachtenRows || [])
    .sort((a, b) => a.sort_order - b.sort_order)
    .map(g => adaptGutachten(g, objekteRows));

  // Unterlagen-Zählung pro Gutachten nachtragen
  const dokByGutachten = {};
  (dokumenteRows || []).forEach(d => {
    if (d.gutachten_id) {
      if (!dokByGutachten[d.gutachten_id]) dokByGutachten[d.gutachten_id] = { done: 0, total: 0 };
      dokByGutachten[d.gutachten_id].total++;
      if (d.extract_status === 'done') dokByGutachten[d.gutachten_id].done++;
    }
  });
  gutachten.forEach(g => {
    const stats = dokByGutachten[g.id] || { done: 0, total: 0 };
    // Platzhalter: wenn noch keine Dokumente angelegt sind, zeige 0/12 als Richtwert
    g.unterlagenDone = stats.done;
    g.unterlagenTotal = stats.total > 0 ? stats.total : 12;
  });

  // Ortstermin-Aggregation für Dashboard
  const ortstermine = gutachten
    .map((g, i) => {
      const row = gutachtenRows.find(r => r.id === g.id);
      if (!row?.ortstermin_datum) return null;
      const ortDisplay = g.adresse.split(',').pop()?.trim() || '';
      const d = formatDate(row.ortstermin_datum);
      const t = row.ortstermin_uhrzeit ? row.ortstermin_uhrzeit.substring(0, 5) : '';
      return `${ortDisplay}: ${d}${t ? ', ' + t : ''}`;
    })
    .filter(Boolean);

  // Letzte Aktivität: MAX aller updated_at/created_at über alle Projekt-Entitäten
  const timestamps = [
    projRow.updated_at,
    ...(gutachtenRows || []).map(r => r.updated_at),
    ...(objekteRows || []).map(r => r.updated_at),
    ...(dokumenteRows || []).map(r => r.updated_at || r.created_at),
    auftragRow?.updated_at,
  ].filter(Boolean).map(t => new Date(t).getTime()).filter(t => !isNaN(t));
  const lastActivity = timestamps.length > 0 ? new Date(Math.max(...timestamps)).toISOString() : null;

  return {
    id: projRow.id,
    name: projRow.name,
    status: projRow.status === 'in_bearbeitung' ? 'in Bearbeitung' :
            projRow.status === 'offen' ? 'offen' :
            projRow.status === 'abgeschlossen' ? 'abgeschlossen' : projRow.status,
    updatedAt: lastActivity || projRow.updated_at || null,
    // updatedBy nur anzeigen wenn Projekt-Row selbst die jüngste Änderung ist
    updatedBy: (timestamps.length > 0 && projRow.updated_at && new Date(projRow.updated_at).getTime() >= Math.max(...timestamps) - 1000)
      ? (projRow.updated_by || null) : null,
    ...adaptAuftrag(auftragRow),
    ortstermin: ortstermine.length > 0 ? ortstermine.join(' · ') : '—',
    beteiligte: adaptBeteiligte(beteiligteRows),
    gutachten,
    dokumente: adaptDokumente(dokumenteRows),
  };
}

// ────────────────────────────────────────────────────────────────────
// Label-Helpers (DB-Enums → Anzeigetexte)
// ────────────────────────────────────────────────────────────────────
function auftragsartLabel(v) {
  return { privat: 'Privatauftrag', gericht: 'Gerichtsauftrag', gutachterausschuss: 'Gutachterausschuss', notariat: 'Notarauftrag' }[v] || v || '';
}
function auftragstypLabel(v) {
  return { verkehrswert: 'Verkehrswertgutachten', bauschaden: 'Bauschadengutachten', miete: 'Mietwertgutachten', minderwert: 'Minderwertgutachten', beleihung: 'Beleihungswertgutachten', marktwert: 'Marktwertindikation', ueberwachung: 'Überwachungsgutachten' }[v] || v || '';
}
function objekttypLabel(v) {
  return { efh: 'EFH', etw: 'Eigentumswohnung', mfh: 'MFH', dhh: 'DHH/Reihenhaus', gewerbe: 'Gewerbe', grundstueck: 'Grundstück', stellplatz: 'Stellplatz', keller: 'Kellerraum' }[v] || v || '';
}
function dokumenttypLabel(v) {
  // 1) Registry ist Single Source of Truth — wenn geladen, deren label nehmen.
  if (_fetchedRegistry && Array.isArray(_fetchedRegistry)) {
    const hit = _fetchedRegistry.find(t => t.id === v);
    if (hit?.label) return hit.label;
  }
  // 2) Lokale Fallback-Map (kürzere Anzeige-Labels für häufige Typen)
  const m = {
    gerichtsbeschluss: 'Beweisbeschluss',
    anschreiben: 'Auftrag / Anschreiben',
    auftragsanfrage: 'Auftragsanfrage',
    grundbuchauszug: 'Grundbuchauszug',
    bauplan: 'Baupläne',
    teilungserklaerung: 'Teilungserklärung',
    bebauungsplan: 'Bebauungsplan',
    baurechtsauskunft: 'Baurechtliche Auskunft',
    bodenrichtwert: 'Bodenrichtwertkarte',
    altlasten: 'Altlastenauskunft',
    demografie: 'Demografische Indikatoren',
    erschliessung_strasse: 'Erschließung (Straße)',
    erschliessung_wasser: 'Erschließung (Wasser)',
    erschliessung_kanal: 'Erschließung (Kanal)',
    denkmalschutz: 'Denkmalschutzauskunft',
    feuerstaette: 'Feuerstättenbescheid',
    gebaeudeversicherung: 'Gebäudeversicherung',
    weg_protokoll: 'WEG-Protokoll',
    weg_hausgeld: 'WEG-Hausgeldabrechnung',
    weg_verwalter: 'WEG-Verwalter',
    weg_wirtschaftsplan: 'WEG-Wirtschaftsplan',
    lagekarte: 'Lagekarte',
    energieausweis: 'Energieausweis',
    mietvertrag: 'Mietvertrag',
  };
  // 3) Letzter Fallback: ID mit großem Anfangsbuchstaben statt roher Kleinschreibung
  return m[v] || (v ? v.charAt(0).toUpperCase() + v.slice(1) : '');
}
function formatDate(iso) {
  if (!iso) return '';
  const [y, m, d] = iso.split('-');
  return `${d}.${m}.${y}`;
}
function daysUntil(iso) {
  if (!iso) return null;
  const target = new Date(iso);
  const now = new Date();
  return Math.ceil((target - now) / 86400000);
}

// ────────────────────────────────────────────────────────────────────
// Daten-Loader
// ────────────────────────────────────────────────────────────────────
async function ladeProjektliste() {
  const sb = await initSupabase();

  // Haupt-Query: Projekte mit eingebetteten Aufträgen und Gutachten.
  // PostgREST erkennt auftraege ggf. als to-one (UNIQUE FK) — dann
  // kommt bei Projekten ohne Auftrag "Cannot coerce". Daher separate
  // Queries als Fallback.
  let data;
  try {
    const res = await sb
      .from('projects')
      .select(`
        id, name, status, created_at,
        auftraege ( id, akte, aktenzeichen, geschaeftszeichen, auftragsart, auftragstyp, verfahrensart, auftraggeber, auftragseingang, beschlussdatum, abgabe_extern, abgabe_intern, sachverstaendiger, sachbearbeiter, notizen, bearbeitungsstatus ),
        gutachten ( id, adresse, sort_order, ortstermin_datum, titelbild_path, bewertungsobjekte ( id, objekttyp, bezeichnung ) ),
        dokumente ( id, extract_status )
      `)
      .is('deleted_at', null)
      .order('created_at', { ascending: false });
    if (res.error) throw res.error;
    data = res.data;
  } catch (embeddedErr) {
    // Fallback: Projekte ohne Embed laden, Aufträge separat
    console.warn('[ladeProjektliste] Embedded query failed, using fallback:', embeddedErr.message);
    const { data: projects, error: pErr } = await sb
      .from('projects')
      .select('id, name, status, created_at')
      .is('deleted_at', null)
      .order('created_at', { ascending: false });
    if (pErr) throw pErr;

    const ids = projects.map(p => p.id);
    const { data: auftraege } = await sb
      .from('auftraege')
      .select('id, project_id, akte, aktenzeichen, geschaeftszeichen, auftragsart, auftragstyp, verfahrensart, auftraggeber, auftragseingang, beschlussdatum, abgabe_extern, abgabe_intern, sachverstaendiger, sachbearbeiter, notizen, bearbeitungsstatus')
      .in('project_id', ids);
    const { data: gutachten } = await sb
      .from('gutachten')
      .select('id, project_id, adresse, sort_order, ortstermin_datum, titelbild_path, bewertungsobjekte ( id, objekttyp, bezeichnung )')
      .in('project_id', ids);
    const { data: dokumente } = await sb
      .from('dokumente')
      .select('id, project_id, extract_status')
      .in('project_id', ids);

    const auftraegeMap = {};
    (auftraege || []).forEach(a => { auftraegeMap[a.project_id] = a; });
    const gutachtenMap = {};
    (gutachten || []).forEach(g => {
      if (!gutachtenMap[g.project_id]) gutachtenMap[g.project_id] = [];
      gutachtenMap[g.project_id].push(g);
    });
    const dokumenteMap = {};
    (dokumente || []).forEach(d => {
      if (!dokumenteMap[d.project_id]) dokumenteMap[d.project_id] = [];
      dokumenteMap[d.project_id].push(d);
    });

    data = projects.map(p => ({
      ...p,
      auftraege: auftraegeMap[p.id] || null,
      gutachten: gutachtenMap[p.id] || [],
      dokumente: dokumenteMap[p.id] || [],
    }));
  }

  return data.map(p => {
    const auftrag = Array.isArray(p.auftraege) ? (p.auftraege[0] || {}) : (p.auftraege || {});
    const gutachtenList = p.gutachten || [];
    const gutachtenCount = gutachtenList.length;
    const allObjekte = gutachtenList.flatMap(g => g.bewertungsobjekte || []);
    const objektCount = allObjekte.length;

    // Adresse: vollständig (erste Gutachten-Adresse)
    const adresse = gutachtenCount > 0 ? (gutachtenList[0].adresse || '') : '';
    const standort = gutachtenCount === 0
      ? ''
      : gutachtenCount === 1
        ? (gutachtenList[0].adresse?.split(',').pop().trim() || '')
        : gutachtenList.map(g => g.adresse?.split(',').pop().trim() || '').join(' + ');

    // Objektart: z.B. "EFH" oder "EFH, Garage"
    const objektarten = [...new Set(allObjekte.map(o => objekttypLabel(o.objekttyp)).filter(Boolean))];
    const objektart = objektarten.join(', ') || '';

    // Dokumente-Zählung
    const doks = Array.isArray(p.dokumente) ? p.dokumente : [];
    const dokTotal = doks.length;
    const dokDone = doks.filter(d => d.extract_status === 'done').length;

    // Effektive Frist
    const effektiveFrist = auftrag.abgabe_extern && auftrag.abgabe_intern
      ? (new Date(auftrag.abgabe_intern) < new Date(auftrag.abgabe_extern) ? auftrag.abgabe_intern : auftrag.abgabe_extern)
      : auftrag.abgabe_extern || auftrag.abgabe_intern || null;
    const fristRest = effektiveFrist ? daysUntil(effektiveFrist) : null;

    // Nächster Ortstermin
    const ortsterminDaten = gutachtenList
      .filter(g => g.ortstermin_datum)
      .map(g => g.ortstermin_datum)
      .sort();
    const naechsterOrtstermin = ortsterminDaten[0] || null;

    return {
      id: p.id,
      name: p.name,
      akte: auftrag.akte || '',
      aktenzeichen: auftrag.aktenzeichen || '',
      geschaeftszeichen: auftrag.geschaeftszeichen || '',
      auftragsart: auftragsartLabel(auftrag.auftragsart),
      auftragstyp: auftragstypLabel(auftrag.auftragstyp),
      verfahren: auftrag.verfahrensart,
      auftraggeber: auftrag.auftraggeber || '',
      adresse,
      standort,
      titelbildPath: (gutachtenList.find(g => g.titelbild_path) || {}).titelbild_path || null,
      objektart,
      interneNotiz: auftrag.notizen || '',
      auftragseingang: auftrag.auftragseingang || null,
      beschlussdatum: auftrag.beschlussdatum || null,
      gutachtenCount,
      objektCount,
      dokTotal,
      dokDone,
      abgabeExtern: auftrag.abgabe_extern || null,
      abgabeIntern: auftrag.abgabe_intern || null,
      ortsterminDatum: naechsterOrtstermin,
      effektiveFrist,
      fristRestDays: fristRest,
      sachverstaendiger: auftrag.sachverstaendiger || null,
      sachbearbeiter: auftrag.sachbearbeiter || null,
      bearbeitungsstatus: auftrag.bearbeitungsstatus || null,
      auftrag_id: auftrag.id || null,
      status: p.status,
      createdAt: p.created_at,
    };
  });
}

// ────────────────────────────────────────────────────────────────────
// createProjekt — legt Projekt + Auftrag + Gutachten + Bewertungsobjekt an
// Parameter:
//   name: string (Pflicht, z.B. "Schmidt, Familie")
//   auftragsart: 'privat' | 'gericht' | 'gutachterausschuss' | 'notariat'
//   auftragstyp: 'verkehrswert' | 'miete' | …
//   optional: auftraggeber, akte, verfahrensart, abgabe_extern, notizen
// ────────────────────────────────────────────────────────────────────
async function createProjekt({
  auftragsart = 'privat',
  auftragstyp = 'verkehrswert',
  auftraggeber = '',
  akte = '',
  verfahrensart = null,
  abgabeExtern = null,
  abgabeIntern = null,
  ortsterminDatum = null,
  erstesGutachtenAdresse = '',
  sachverstaendiger = null,
  sachverstaendigerKuerzel = null,
  kiFlow = false,
}) {
  const sb = await initSupabase();

  // 1) organization_id + Kuerzel des aktuellen Users
  const { data: { user }, error: userErr } = await sb.auth.getUser();
  if (userErr || !user) throw new Error('Nicht angemeldet');

  const { data: profile, error: profErr } = await sb
    .from('user_profiles')
    .select('organization_id, id, akte_kuerzel')
    .eq('id', user.id)
    .single();
  if (profErr || !profile) throw new Error('User-Profil nicht gefunden');

  // 2) Internes Aktenzeichen: bei KI-Flow erst nach Extraktion (SV-Name bestimmt Kürzel)
  let akteIntern;
  let laufendeNr = null;
  if (kiFlow) {
    // Temporärer Name, wird nach Extraktion durch echtes AZ ersetzt
    akteIntern = 'Neuer Auftrag';
  } else {
    // Kürzel des ausgewählten Sachverständigen bevorzugen (manueller Wizard).
    // Fallback auf das Kürzel des eingeloggten Users (Standard-Workflow).
    const kuerzel = sachverstaendigerKuerzel || profile.akte_kuerzel || '??';
    try {
      const { data: azData, error: azErr } = await sb.rpc('generate_aktenzeichen_intern', {
        p_org_id: profile.organization_id,
        p_kuerzel: kuerzel,
      });
      if (azErr) throw azErr;
      akteIntern = azData;
      const nrMatch = (azData || '').match(/^(\d+)\//);
      if (nrMatch) laufendeNr = parseInt(nrMatch[1], 10);
    } catch (e) {
      console.warn('[createProjekt] Auto-AZ fehlgeschlagen, Fallback:', e);
      const jahr = new Date().getFullYear().toString().slice(-2);
      akteIntern = '?/' + jahr + '/' + kuerzel;
    }
  }

  // 3) Projekt anlegen
  const { data: newProjekt, error: projErr } = await sb
    .from('projects')
    .insert({
      organization_id: profile.organization_id,
      name: akteIntern,
      laufende_nr: laufendeNr,
      status: 'offen',
    })
    .select()
    .single();
  if (projErr) throw new Error('Auftrag anlegen fehlgeschlagen: ' + projErr.message);

  const projektId = newProjekt.id;

  try {
    // 3) Auftrag anlegen (1:1 zum Projekt)
    const auftragRow = {
      project_id: projektId,
      auftragsart,
      auftragstyp,
      auftraggeber: auftraggeber?.trim() || null,
      akte: akte?.trim() || null,
      verfahrensart: verfahrensart || null,
      abgabe_extern: abgabeExtern || null,
      abgabe_intern: abgabeIntern || null,
      // Im KI-Flow kein Auftragseingang vorbelegen — die Extraktion liefert
      // das echte Datum aus dem Dokument. Sonst entsteht ein Phantom-Konflikt
      // beim Apply ("20.04.2026" vs. "27.10.2025").
      auftragseingang: kiFlow ? null : new Date().toISOString().split('T')[0],
      // Ausfertigungen explizit null: die DB hat einen Default von 1,
      // aber der Wert soll erst durch Extraktion oder manuelle Eingabe kommen.
      ausfertigungen: null,
      // Manueller Anlege-Flow: SV-Name aus User-Profil mitgeben.
      // Im KI-Flow null lassen, damit die Extraktion den Wert aus Beschluss/
      // Anschreiben liefern kann (oder leer bleibt — User füllt im StammdatenTab).
      sachverstaendiger: sachverstaendiger?.trim() || null,
      // Manueller Workflow-Status: neuer Auftrag startet bei "01 Auftrag angelegt".
      bearbeitungsstatus: 'angelegt',
      // ausfertigungen bewusst nicht setzen — wird durch Anschreiben-
      // Extraktion oder manuelle Eingabe gefüllt. Kein "1× Druck"-Phantom-
      // wert in der UI bei frisch angelegten Projekten.
    };
    const { error: aufErr } = await sb.from('auftraege').insert(auftragRow);
    if (aufErr) throw new Error('Auftrag anlegen fehlgeschlagen: ' + aufErr.message);

    // 4) Erstes Gutachten anlegen (optional mit Adresse)
    const { data: newGut, error: gutErr } = await sb
      .from('gutachten')
      .insert({
        project_id: projektId,
        art: auftragstyp === 'bauschaden' ? 'bauschaden' : 'verkehrswert',
        titel: `${akteIntern} ${auftragstypLabel(auftragstyp)}`,
        adresse: erstesGutachtenAdresse?.trim() || null,
        sort_order: 0,
        ortstermin_datum: ortsterminDatum || null,
      })
      .select()
      .single();
    if (gutErr) throw new Error('Gutachten anlegen fehlgeschlagen: ' + gutErr.message);

    // 5) Erstes Bewertungsobjekt anlegen (leer, kann später ausgefüllt werden)
    // Hinweis: bewertungsobjekte hat KEIN project_id — die Verbindung läuft über gutachten_id.
    const { error: objErr } = await sb.from('bewertungsobjekte').insert({
      gutachten_id: newGut.id,
      bezeichnung: 'Objekt 1',
      sort_order: 0,
    });
    if (objErr) throw new Error('Bewertungsobjekt anlegen fehlgeschlagen: ' + objErr.message);

    return projektId;
  } catch (err) {
    // Bei Fehler: Projekt wieder löschen (Cascade räumt auch abhängige Zeilen auf)
    try { await sb.from('projects').delete().eq('id', projektId); } catch {}
    throw err;
  }
}

async function ladeProjektDetail(projektId) {
  const sb = await initSupabase();

  // Parallel laden — maybeSingle für projects, damit gelöschte Projekte
  // kein 406 werfen (Realtime feuert auch nach DELETE)
  const [projRes, auftragRes, beteiligteRes, gutachtenRes, dokumenteRes] = await Promise.all([
    sb.from('projects').select('*').eq('id', projektId).maybeSingle(),
    sb.from('auftraege').select('*').eq('project_id', projektId).maybeSingle(),
    sb.from('beteiligte').select('*').eq('project_id', projektId).order('sort_order'),
    sb.from('gutachten').select('*').eq('project_id', projektId).order('sort_order'),
    sb.from('dokumente').select('*').eq('project_id', projektId).order('sort_order'),
  ]);

  if (projRes.error) throw projRes.error;
  // Projekt wurde gelöscht → null zurückgeben statt crashen
  if (!projRes.data) return null;
  if (gutachtenRes.error) throw gutachtenRes.error;

  // Bewertungsobjekte aller Gutachten dieses Projekts
  const gutachtenIds = (gutachtenRes.data || []).map(g => g.id);
  let objekteRows = [];
  if (gutachtenIds.length > 0) {
    const { data, error } = await sb
      .from('bewertungsobjekte')
      .select('*')
      .in('gutachten_id', gutachtenIds)
      .order('sort_order');
    if (error) throw error;
    objekteRows = data || [];
  }

  const _projekt = adaptProjekt(
    projRes.data,
    auftragRes.data,
    beteiligteRes.data,
    gutachtenRes.data,
    objekteRows,
    dokumenteRes.data
  );
  // Bauschaden: Beweisfragen je Gutachten nachladen und anhängen
  if (_projekt && gutachtenIds.length > 0) {
    const { data: bfRows } = await sb
      .from('beweisfragen').select('*').in('gutachten_id', gutachtenIds).order('sortier_index');
    const _byG = {};
    (bfRows || []).forEach(b => { (_byG[b.gutachten_id] = _byG[b.gutachten_id] || []).push(b); });
    (_projekt.gutachten || []).forEach(g => { g.beweisfragen = _byG[g.id] || []; });
  }
  return _projekt;
}

// ────────────────────────────────────────────────────────────────────
// Row-CRUD-Client
// Wrapper um /api/row und /api/project/* — einheitliche Fehlerbehandlung.
// ────────────────────────────────────────────────────────────────────
async function apiPatchRow(table, id, patch, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/row`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${session.access_token}`,
    },
    body: JSON.stringify({ table, id, patch }),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }
  return await res.json();
}

async function apiInsertRow(table, row, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/row`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${session.access_token}`,
    },
    body: JSON.stringify({ table, row }),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }
  return await res.json();
}

async function apiDeleteRow(table, id, session, workerUrl) {
  const res = await fetch(
    `${workerUrl}/api/row?table=${encodeURIComponent(table)}&id=${encodeURIComponent(id)}`,
    { method: 'DELETE', headers: { Authorization: `Bearer ${session.access_token}` } }
  );
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }
  return await res.json();
}

async function apiArchiveProjekt(projektId, archive, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/project/archive`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${session.access_token}`,
    },
    body: JSON.stringify({ id: projektId, archive }),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }
  return await res.json();
}

async function apiDeleteProjekt(projektId, session, workerUrl) {
  const res = await fetch(
    `${workerUrl}/api/project?id=${encodeURIComponent(projektId)}`,
    { method: 'DELETE', headers: { Authorization: `Bearer ${session.access_token}` } }
  );
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }
  return await res.json();
}

async function apiDeleteGutachten(gutachtenId, session, workerUrl) {
  const res = await fetch(
    `${workerUrl}/api/gutachten?id=${encodeURIComponent(gutachtenId)}`,
    { method: 'DELETE', headers: { Authorization: `Bearer ${session.access_token}` } }
  );
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }
  return await res.json();
}

async function apiDeleteDokument(dokumentId, session, workerUrl) {
  const res = await fetch(
    `${workerUrl}/api/document?id=${encodeURIComponent(dokumentId)}`,
    { method: 'DELETE', headers: { Authorization: `Bearer ${session.access_token}` } }
  );
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }
  return await res.json();
}

// ────────────────────────────────────────────────────────────────────
// Anschreiben-System: Kontakte / Vorlagen / Briefvorlagen / Render
// ────────────────────────────────────────────────────────────────────
async function apiListKontakte(session, workerUrl, params = {}) {
  const q = new URLSearchParams();
  if (params.typ) q.set('typ', params.typ);
  if (params.gemeinde) q.set('gemeinde', params.gemeinde);
  const url = `${workerUrl}/api/kontakte${q.toString() ? '?' + q.toString() : ''}`;
  const res = await fetch(url, { headers: { Authorization: `Bearer ${session.access_token}` } });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return (await res.json()).kontakte || [];
}
async function apiSaveKontakt(kontakt, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/kontakte`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
    body: JSON.stringify(kontakt),
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return (await res.json()).kontakt;
}
async function apiDeleteKontakt(id, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/kontakte?id=${encodeURIComponent(id)}`, {
    method: 'DELETE', headers: { Authorization: `Bearer ${session.access_token}` },
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return await res.json();
}
async function apiListVorlagen(session, workerUrl, typ = null) {
  const url = `${workerUrl}/api/anschreiben-vorlagen${typ ? '?typ=' + encodeURIComponent(typ) : ''}`;
  const res = await fetch(url, { headers: { Authorization: `Bearer ${session.access_token}` } });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return (await res.json()).vorlagen || [];
}
async function apiSaveVorlage(vorlage, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/anschreiben-vorlagen`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
    body: JSON.stringify(vorlage),
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return (await res.json()).vorlage;
}
async function apiDeleteVorlage(id, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/anschreiben-vorlagen?id=${encodeURIComponent(id)}`, {
    method: 'DELETE', headers: { Authorization: `Bearer ${session.access_token}` },
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return await res.json();
}
async function apiCloneVorlage(vorlageId, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/anschreiben-vorlagen/clone`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
    body: JSON.stringify({ vorlage_id: vorlageId }),
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return (await res.json()).vorlage;
}
async function apiListBriefvorlagen(session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/briefvorlagen`, {
    headers: { Authorization: `Bearer ${session.access_token}` },
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return (await res.json()).briefvorlagen || [];
}
async function apiUploadBriefvorlage(file, name, session, workerUrl) {
  const form = new FormData();
  form.append('file', file);
  form.append('name', name || 'Briefvorlage');
  const res = await fetch(`${workerUrl}/api/briefvorlagen`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${session.access_token}` },
    body: form,
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return (await res.json()).briefvorlage;
}
async function apiDeleteBriefvorlage(id, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/briefvorlagen?id=${encodeURIComponent(id)}`, {
    method: 'DELETE', headers: { Authorization: `Bearer ${session.access_token}` },
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return await res.json();
}
async function apiRenderAnschreiben(auftragId, vorlageId, kontaktId, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/anschreiben/render`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
    body: JSON.stringify({ auftrag_id: auftragId, vorlage_id: vorlageId, kontakt_id: kontaktId }),
  });
  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
  return await res.blob();
}

// Bulk-Render: mehrere Anschreiben parallel → ZIP-Datei
// items: [{ vorlage_id, kontakt_id }, ...]
// Returns: { blob, succeeded, failed } (blob ist ZIP, header werten Erfolgsquote aus)
async function apiRenderAnschreibenBulk(auftragId, items, session, workerUrl) {
  const res = await fetch(`${workerUrl}/api/anschreiben/render-bulk`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
    body: JSON.stringify({ auftrag_id: auftragId, items }),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }
  return {
    blob: await res.blob(),
    succeeded: parseInt(res.headers.get('x-bulk-success') || '0', 10),
    failed: parseInt(res.headers.get('x-bulk-failed') || '0', 10),
  };
}

// ────────────────────────────────────────────────────────────────────
// Feld-Herkunft laden (Welcher Wert kommt aus welchem Dokument?)
// Liefert eine Map: "tabelle:rowId:feld" → { dokument_id, titel, typ, applied_at }
// ────────────────────────────────────────────────────────────────────
async function ladeHerkunft(projektId, session, workerUrl) {
  if (!projektId || !session?.access_token) return {};
  try {
    let token = session.access_token;
    try {
      const sb = await initSupabase();
      const { data } = await sb.auth.getSession();
      if (data?.session?.access_token) token = data.session.access_token;
    } catch {}
    const res = await fetch(
      `${workerUrl}/api/feld-herkunft?project_id=${encodeURIComponent(projektId)}`,
      { headers: { Authorization: `Bearer ${token}` } }
    );
    if (!res.ok) return {};
    const data = await res.json();
    const map = {};
    for (const h of (data.herkunft || [])) {
      const key = `${h.tabelle}:${h.row_id}:${h.feld}`;
      map[key] = {
        dokument_id: h.dokument_id,
        titel: h.dokumente?.titel || 'Dokument',
        typ: h.dokumente?.typ || '',
        applied_at: h.applied_at,
      };
    }
    return map;
  } catch (e) {
    console.warn('Herkunft-Load failed:', e);
    return {};
  }
}

// ────────────────────────────────────────────────────────────────────
// Feld-History laden (Audit-Timeline pro Feld).
// Worker: GET /api/feld-history liefert Array aus der feld_history-View:
//   { id, tabelle, row_id, feld, old_value, new_value, change_source,
//     change_reason, changed_at, changed_by, changed_by_name,
//     changed_by_email, dokument_id, dokument_titel, dokument_typ }
// Sortiert nach changed_at DESC (neueste zuerst).
// ────────────────────────────────────────────────────────────────────
async function ladeFeldHistory({ projektId, tabelle, rowId, feld, session, workerUrl, limit = 50 }) {
  if (!projektId || !session?.access_token) return [];
  try {
    const params = new URLSearchParams();
    params.set('project_id', projektId);
    if (tabelle) params.set('tabelle', tabelle);
    if (rowId)   params.set('row_id', rowId);
    if (feld)    params.set('feld', feld);
    params.set('limit', String(limit));
    const res = await fetch(
      `${workerUrl}/api/feld-history?${params.toString()}`,
      { headers: { Authorization: `Bearer ${session.access_token}` } }
    );
    if (!res.ok) return [];
    const data = await res.json();
    return Array.isArray(data.history) ? data.history : [];
  } catch (e) {
    console.warn('Feld-History-Load failed:', e);
    return [];
  }
}

// ────────────────────────────────────────────────────────────────────
// Dokument-Ansicht: Signed-URL holen und in neuem Tab öffnen
// ────────────────────────────────────────────────────────────────────
async function oeffneDokument(dokumentId, session, workerUrl) {
  if (!dokumentId || !session?.access_token) return;
  try {
    const res = await fetch(
      `${workerUrl}/api/document-url?id=${encodeURIComponent(dokumentId)}`,
      { headers: { Authorization: `Bearer ${session.access_token}` } }
    );
    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      alert('Dokument konnte nicht geöffnet werden: ' + (err.error || res.statusText));
      return;
    }
    const data = await res.json();
    if (data.url) {
      window.open(data.url, '_blank', 'noopener,noreferrer');
    }
  } catch (e) {
    alert('Fehler beim Öffnen: ' + (e.message || e));
  }
}

// Wie oeffneDokument, aber gibt die URL zurück statt Tab zu öffnen.
// Für eingebetteten PDF-Viewer (iframe).
async function ladeDokumentUrl(dokumentId, session, workerUrl) {
  if (!dokumentId || !session?.access_token) return null;
  try {
    const res = await fetch(
      `${workerUrl}/api/document-url?id=${encodeURIComponent(dokumentId)}`,
      { headers: { Authorization: `Bearer ${session.access_token}` } }
    );
    if (!res.ok) return null;
    const data = await res.json();
    return data.url || null;
  } catch {
    return null;
  }
}

// ────────────────────────────────────────────────────────────────────
// Notes-Datenlader (Ortstermin-Notizen)
// Supabase-RLS schützt automatisch auf Organisations-Ebene
// ────────────────────────────────────────────────────────────────────
// ────────────────────────────────────────────────────────────────────
// ORTSTERMIN INDEXEDDB — Offline-First Audio + Notes Storage
// Prinzip: Jedes Audio-Segment und jede Notiz wird SOFORT in IndexedDB
// gesichert. Supabase-Sync ist ein nachgelagerter Bonus.
// ────────────────────────────────────────────────────────────────────
const IDB_NAME = 'augenschein-ortstermin';
const IDB_VERSION = 1;
const STORE_AUDIO = 'audio-chunks';
const STORE_PENDING = 'pending-notes';

function openOrtsterminDB() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(IDB_NAME, IDB_VERSION);
    req.onupgradeneeded = (e) => {
      const db = e.target.result;
      if (!db.objectStoreNames.contains(STORE_AUDIO)) {
        const s = db.createObjectStore(STORE_AUDIO, { keyPath: 'id' });
        s.createIndex('gutachten_id', 'gutachten_id', { unique: false });
        s.createIndex('status', 'status', { unique: false });
      }
      if (!db.objectStoreNames.contains(STORE_PENDING)) {
        const s = db.createObjectStore(STORE_PENDING, { keyPath: 'localId' });
        s.createIndex('gutachten_id', 'gutachten_id', { unique: false });
        s.createIndex('syncStatus', 'syncStatus', { unique: false });
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

async function idbPut(storeName, item) {
  const db = await openOrtsterminDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, 'readwrite');
    tx.objectStore(storeName).put(item);
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

async function idbGetAll(storeName, indexName, key) {
  const db = await openOrtsterminDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, 'readonly');
    const store = tx.objectStore(storeName);
    const req = indexName
      ? store.index(indexName).getAll(key)
      : store.getAll();
    req.onsuccess = () => resolve(req.result || []);
    req.onerror = () => reject(req.error);
  });
}

async function idbDelete(storeName, key) {
  const db = await openOrtsterminDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(storeName, 'readwrite');
    tx.objectStore(storeName).delete(key);
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

// ────────────────────────────────────────────────────────────────────
// useOrtsterminOffline — Hook für offline-first Notes + Audio
// ────────────────────────────────────────────────────────────────────
function useOrtsterminOffline(gutachtenId) {
  const [pendingCount, setPendingCount] = useState(0);
  const [audioChunks, setAudioChunks] = useState([]);
  const [isSyncing, setIsSyncing] = useState(false);
  const isSyncingRef = useRef(false);
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  // Online/Offline Listener
  useEffect(() => {
    const goOnline = () => setIsOnline(true);
    const goOffline = () => setIsOnline(false);
    window.addEventListener('online', goOnline);
    window.addEventListener('offline', goOffline);
    return () => {
      window.removeEventListener('online', goOnline);
      window.removeEventListener('offline', goOffline);
    };
  }, []);

  // Pending-Count aktualisieren
  const refreshPending = useCallback(async () => {
    if (!gutachtenId) return;
    try {
      const pending = await idbGetAll(STORE_PENDING, 'gutachten_id', gutachtenId);
      const unsent = pending.filter(n => n.syncStatus === 'pending');
      setPendingCount(unsent.length);
    } catch { setPendingCount(0); }
  }, [gutachtenId]);

  // Audio-Chunks laden
  const refreshAudio = useCallback(async () => {
    if (!gutachtenId) return;
    try {
      const chunks = await idbGetAll(STORE_AUDIO, 'gutachten_id', gutachtenId);
      setAudioChunks(chunks.sort((a, b) => a.timestamp - b.timestamp));
    } catch { setAudioChunks([]); }
  }, [gutachtenId]);

  useEffect(() => { refreshPending(); refreshAudio(); }, [refreshPending, refreshAudio]);

  // ─── Audio-Chunk speichern (sofort in IDB) ───
  const saveAudioChunk = useCallback(async (blob, metadata = {}) => {
    const chunk = {
      id: `audio_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
      gutachten_id: gutachtenId,
      blob,
      mimeType: blob.type || 'audio/webm',
      sizeBytes: blob.size,
      timestamp: Date.now(),
      durationMs: metadata.durationMs || 0,
      tagId: metadata.tagId || null,
      objektId: metadata.objektId || null,
      status: 'saved', // saved → transcribed → synced
      transcription: null,
    };
    await idbPut(STORE_AUDIO, chunk);
    await refreshAudio();
    return chunk.id;
  }, [gutachtenId, refreshAudio]);

  // ─── Notiz offline speichern (IDB-first, Supabase-Sync später) ───
  const saveNoteOffline = useCallback(async (noteRow) => {
    const localItem = {
      localId: `note_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
      gutachten_id: gutachtenId,
      noteRow: { ...noteRow, gutachten_id: gutachtenId },
      syncStatus: 'pending', // pending → synced
      createdAt: Date.now(),
      serverId: null,
    };
    await idbPut(STORE_PENDING, localItem);
    await refreshPending();
    return localItem.localId;
  }, [gutachtenId, refreshPending]);

  // ─── Bulk-Save für Rundgang-Segmente ───
  const saveNotesOfflineBulk = useCallback(async (noteRows) => {
    const ids = [];
    for (const row of noteRows) {
      const id = await saveNoteOffline(row);
      ids.push(id);
    }
    return ids;
  }, [saveNoteOffline]);

  // ─── Sync: Pending Notes → Supabase ───
  const syncPendingNotes = useCallback(async () => {
    if (!gutachtenId || isSyncingRef.current) return { synced: 0, failed: 0 };
    isSyncingRef.current = true;
    setIsSyncing(true);
    let synced = 0, failed = 0;
    try {
      const pending = await idbGetAll(STORE_PENDING, 'gutachten_id', gutachtenId);
      const unsent = pending.filter(n => n.syncStatus === 'pending');
      for (const item of unsent) {
        try {
          const serverNote = await insertNote(item.noteRow);
          item.syncStatus = 'synced';
          item.serverId = serverNote.id;
          await idbPut(STORE_PENDING, item);
          synced++;
        } catch (e) {
          console.warn('[Sync] Note failed:', e);
          failed++;
        }
      }
      await refreshPending();
    } finally { isSyncingRef.current = false; setIsSyncing(false); }
    return { synced, failed };
  }, [gutachtenId, refreshPending]);

  // ─── Cleanup: Synced Items sofort entfernen, alte Audio-Blobs aufräumen ───
  const cleanupSynced = useCallback(async () => {
    const pending = await idbGetAll(STORE_PENDING, 'gutachten_id', gutachtenId);
    for (const item of pending) {
      if (item.syncStatus === 'synced') {
        await idbDelete(STORE_PENDING, item.localId);
      }
    }
    // Audio-Cleanup: synced Chunks sofort, alte Chunks nach 24h, stale Safety-Blobs nach 1h
    const chunks = await idbGetAll(STORE_AUDIO, 'gutachten_id', gutachtenId);
    const now = Date.now();
    for (const chunk of chunks) {
      const ageMs = now - (chunk.timestamp || 0);
      if (chunk.status === 'synced') {
        await idbDelete(STORE_AUDIO, chunk.id);
      } else if (chunk.status === 'recording' && ageMs > 3600000) {
        console.log('[Cleanup] Removing stale safety blob:', chunk.id);
        await idbDelete(STORE_AUDIO, chunk.id);
      } else if (ageMs > 86400000) {
        console.log('[Cleanup] Removing old audio chunk:', chunk.id, 'age:', Math.round(ageMs / 3600000), 'h');
        await idbDelete(STORE_AUDIO, chunk.id);
      }
    }
  }, [gutachtenId]);

  // ─── Auto-Sync wenn online ───
  useEffect(() => {
    if (isOnline && pendingCount > 0 && !isSyncing) {
      syncPendingNotes().then(() => cleanupSynced());
    }
  }, [isOnline, pendingCount, isSyncing, syncPendingNotes, cleanupSynced]);

  // ─── Periodischer Retry: alle 30s pending Notes erneut versuchen ───
  useEffect(() => {
    if (!isOnline || pendingCount === 0) return;
    const retryInterval = setInterval(() => {
      if (!isSyncing && pendingCount > 0) {
        syncPendingNotes().then(() => cleanupSynced());
      }
    }, 30000);
    return () => clearInterval(retryInterval);
  }, [isOnline, pendingCount, isSyncing, syncPendingNotes, cleanupSynced]);

  // ─── Transcription Queue: Audio-Chunks serverseitig transkribieren ───
  const [transcribeQueue, setTranscribeQueue] = useState(0);
  const isTranscribingRef = useRef(false);

  const refreshTranscribeQueue = useCallback(async () => {
    if (!gutachtenId) return;
    try {
      const chunks = await idbGetAll(STORE_AUDIO, 'gutachten_id', gutachtenId);
      const untranscribed = chunks.filter(c => c.status === 'saved');
      setTranscribeQueue(untranscribed.length);
    } catch { setTranscribeQueue(0); }
  }, [gutachtenId]);

  useEffect(() => { refreshTranscribeQueue(); }, [refreshTranscribeQueue]);

  const processTranscribeQueue = useCallback(async (workerUrl, session) => {
    if (!gutachtenId || !workerUrl || !session?.access_token) return { done: 0, failed: 0 };
    if (isTranscribingRef.current) return { done: 0, failed: 0 };
    isTranscribingRef.current = true;

    let done = 0, failed = 0;
    try {
      const chunks = await idbGetAll(STORE_AUDIO, 'gutachten_id', gutachtenId);
      const queue = chunks
        .filter(c => c.status === 'saved' && c.blob && c.blob.size > 1024)
        .sort((a, b) => a.timestamp - b.timestamp);

      for (const chunk of queue) {
        try {
          // Retry-Limit: Nach 3 Fehlversuchen überspringen
          const retries = chunk.retryCount || 0;
          if (retries >= 3) {
            console.warn('[TranscribeQueue] Skipping chunk after 3 failures:', chunk.id);
            chunk.status = 'failed';
            await idbPut(STORE_AUDIO, chunk);
            failed++;
            continue;
          }

          const ext = (chunk.mimeType || '').includes('webm') ? 'webm'
            : (chunk.mimeType || '').includes('mp4') ? 'mp4' : 'webm';
          const formData = new FormData();
          formData.append('audio', chunk.blob, `chunk-${chunk.id}.${ext}`);
          formData.append('context', JSON.stringify({ profession: 'bewertung' }));

          const res = await fetch(`${workerUrl}/api/transcribe`, {
            method: 'POST',
            headers: { Authorization: `Bearer ${session.access_token}` },
            body: formData,
          });

          if (res.ok) {
            const data = await res.json();
            chunk.transcription = data.text || '';
            chunk.status = 'transcribed';
            chunk.retryCount = 0;
            await idbPut(STORE_AUDIO, chunk);
            done++;
          } else {
            const errBody = await res.json().catch(() => ({}));
            const errMsg = errBody.message || errBody.error || `HTTP ${res.status}`;
            console.warn('[TranscribeQueue] Failed for chunk', chunk.id, '–', errMsg);
            // Bei 400 (korrupte Datei): sofort als failed markieren, nicht wiederholen
            if (res.status === 400) {
              chunk.status = 'failed';
              chunk.errorMessage = errMsg;
              await idbPut(STORE_AUDIO, chunk);
            } else {
              chunk.retryCount = retries + 1;
              await idbPut(STORE_AUDIO, chunk);
            }
            failed++;
          }
        } catch (e) {
          console.warn('[TranscribeQueue] Error:', e.message);
          failed++;
        }
      }
      await refreshTranscribeQueue();
      await refreshAudio();
    } finally {
      isTranscribingRef.current = false;
    }
    return { done, failed };
  }, [gutachtenId, refreshTranscribeQueue, refreshAudio]);

  // Auto-Transcribe wenn online und Queue nicht leer
  useEffect(() => {
    if (isOnline && transcribeQueue > 0 && !isTranscribingRef.current) {
      // Wird vom OrtsterminTab getriggert mit workerUrl/session
      // (Auto-Trigger nur wenn explizit aufgerufen)
    }
  }, [isOnline, transcribeQueue]);

  return {
    pendingCount, audioChunks, isSyncing, isOnline,
    transcribeQueue, processTranscribeQueue,
    saveAudioChunk, saveNoteOffline, saveNotesOfflineBulk,
    syncPendingNotes, cleanupSynced, refreshPending, refreshAudio,
  };
}

async function ladeNotes(gutachtenId) {
  if (!gutachtenId) return [];
  const sb = await initSupabase();
  const { data, error } = await sb
    .from('notes')
    .select('*')
    .eq('gutachten_id', gutachtenId)
    .order('sort_order', { ascending: true })
    .order('created_at', { ascending: true });
  if (error) {
    console.warn('Notes load failed:', error.message);
    return [];
  }
  return data || [];
}

async function insertNote(noteRow) {
  const sb = await initSupabase();

  // Duplikat-Prüfung: gleicher Text + gutachten_id innerhalb der letzten 2 Minuten?
  if (noteRow.gutachten_id && noteRow.text) {
    try {
      const twoMinAgo = new Date(Date.now() - 120000).toISOString();
      const { data: existing } = await sb
        .from('notes')
        .select('id')
        .eq('gutachten_id', noteRow.gutachten_id)
        .eq('text', noteRow.text)
        .gte('created_at', twoMinAgo)
        .limit(1);
      if (existing && existing.length > 0) {
        console.log('[insertNote] Duplikat erkannt, überspringe Insert');
        return existing[0]; // Bestehende Note zurückgeben statt neu einfügen
      }
    } catch (e) {
      console.warn('[insertNote] Dedup-Check fehlgeschlagen:', e.message);
      // Bei Fehler trotzdem einfügen (besser doppelt als gar nicht)
    }
  }

  const { data, error } = await sb
    .from('notes')
    .insert(noteRow)
    .select()
    .single();
  if (error) throw error;
  return data;
}

async function updateNote(id, patch) {
  const sb = await initSupabase();
  const { data, error } = await sb
    .from('notes')
    .update(patch)
    .eq('id', id)
    .select()
    .single();
  if (error) throw error;
  return data;
}

async function deleteNote(id) {
  const sb = await initSupabase();
  const { error } = await sb.from('notes').delete().eq('id', id);
  if (error) throw error;
}

async function insertNotesBulk(rows) {
  if (!rows || rows.length === 0) return [];
  const sb = await initSupabase();
  const { data, error } = await sb.from('notes').insert(rows).select();
  if (error) throw error;
  return data || [];
}

// ────────────────────────────────────────────────────────────────────
// Foto-Upload: Blob → Worker → storage_path → öffentliche URL via signed-url
// ────────────────────────────────────────────────────────────────────
async function uploadPhotoBlob(blob, projektId, gutachtenId, session, workerUrl) {
  const formData = new FormData();
  formData.append('file', blob, `photo-${Date.now()}.jpg`);
  formData.append('project_id', projektId);
  formData.append('gutachten_id', gutachtenId);
  const res = await fetch(`${workerUrl}/api/photo-upload`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${session.access_token}` },
    body: formData,
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `Upload fehlgeschlagen: ${res.status}`);
  }
  return await res.json();  // { photo_id, storage_path, size }
}

// Alle möglichen Beteiligten-Rollen für den "+ Hinzufügen"-Menü
const BETEILIGTE_ROLLEN_VORSCHLAEGE = [
  'Gericht', 'Richter', 'Rechtspfleger',
  'betreibender Gläubiger', 'betreibende Gläubigerin', 'Gläubiger', 'Gläubigerin', 'RA Gläubiger', 'RA Gläubigerin',
  'Schuldner', 'Schuldnerin', 'RA Schuldner', 'RA Schuldnerin',
  'Antragsteller', 'Antragstellerin', 'Antragsgegner', 'Antragsgegnerin',
  'RA Antragsteller', 'RA Antragstellerin', 'RA Antragsgegner', 'RA Antragsgegnerin',
  'Kläger', 'Klägerin', 'Beklagter', 'Beklagte', 'RA Kläger', 'RA Klägerin', 'RA Beklagter', 'RA Beklagte',
  'Notar', 'Notarin', 'Zwangsverwalter', 'Zwangsverwalterin', 'Insolvenzverwalter', 'Insolvenzverwalterin',
  'Käufer', 'Käuferin', 'Mieter', 'Mieterin', 'Streithelfer', 'Streithelferin',
  'Urkundsbeamter', 'Urkundsbeamtin',
  'Sonstiger Beteiligter', 'Sonstige Beteiligte',
];

// ══════════════════════════════════════════════════════════════════
// 2.5 · ORTSTERMIN-CORE (Voice-Recording, Rundgang, Kapitel)
// Portiert aus der Augenschein-Ortstermin-App für V&L-Immobilien
// ══════════════════════════════════════════════════════════════════

// ────────────────────────────────────────────────────────────────────
// V&L-Kapitel-Tags (Immobilien-VWG-Struktur)
// Unterschied zur Alt-App: spezifisch für Verkehrswertgutachten
// ────────────────────────────────────────────────────────────────────
const VL_KAPITEL_TAGS = [
  { id: 'anwesende', label: 'Anwesende Personen', rooms: ['*'] },
  { id: 'aussen', label: 'Außen / Grundstück', rooms: ['*'] },
  { id: 'grundstueck', label: 'Grundstück', rooms: ['*'] },
  { id: 'gebaeude', label: 'Gebäude (außen)', rooms: ['*'] },
  { id: 'dach', label: 'Dach', rooms: ['*'] },
  { id: 'keller', label: 'Kellergeschoss', rooms: ['keller'] },
  { id: 'eg', label: 'Erdgeschoss', rooms: ['eg'] },
  { id: 'og1', label: '1. Obergeschoss', rooms: ['1og'] },
  { id: 'og2', label: '2. Obergeschoss', rooms: ['2og'] },
  { id: 'dg', label: 'Dachgeschoss', rooms: ['dg'] },
  { id: 'sp', label: 'Spitzboden', rooms: ['dg'] },
  { id: 'garage', label: 'Garage / Stellplatz', rooms: ['*'] },
  { id: 'technik_heizung', label: 'Technik — Heizung', rooms: ['keller', 'eg'] },
  { id: 'technik_elektro', label: 'Technik — Elektro', rooms: ['*'] },
  { id: 'technik_wasser', label: 'Technik — Wasser/Sanitär', rooms: ['*'] },
  { id: 'maengel', label: 'Mängel / Schäden', rooms: ['*'] },
  { id: 'wohnwert', label: 'Wohnwertprüfung', rooms: ['*'] },
];

// ════════════════════════════════════════════════════════════════════
// RUNDGANG-PROFILE — Single Source of Truth für "welche Bereiche sind
// bei diesem Objekt vor Ort zu erfassen".
//
// Bisher wurde diese Logik an DREI Stellen unabhängig (und teils
// widersprüchlich) abgeleitet: Briefing (nach objekttyp), Aufnahme-Tab
// und PHOTO_AREAS (beide nach geschosse). Jetzt zentral hier.
//
// getRundgangBereiche(obj) ist die EINZIGE Funktion, die diese Frage
// beantwortet. Sie kombiniert objekttyp (welche Bereichsklassen) mit der
// konkreten geschosse-Konfiguration des Objekts (welche Etagen).
//
// eigentum-Markierung (nur bei ETW relevant):
//   'se' = Sondereigentum (die Wohnung selbst)
//   'ge' = Gemeinschaftseigentum (Gebäudehülle, Dach, zentrale Technik)
//   null = nicht zutreffend (Einzelobjekt ohne WEG-Trennung)
// ════════════════════════════════════════════════════════════════════

// Geschoss-Tags in fester Reihenfolge, gefiltert nach obj.geschosse
const GESCHOSS_TAG_ORDER = [
  { id: 'keller', flag: 'kg', label: 'Kellergeschoss' },
  { id: 'eg',     flag: 'eg', label: 'Erdgeschoss' },
  { id: 'og1',    flag: 'og1', label: '1. Obergeschoss' },
  { id: 'og2',    flag: 'og2', label: '2. Obergeschoss' },
  { id: 'dg',     flag: 'dg', label: 'Dachgeschoss' },
  { id: 'sp',     flag: 'sp', label: 'Spitzboden' },
];

const OBJEKT_RUNDGANG_PROFILE = {
  // Unbebautes Grundstück: kein Gebäude, keine Geschosse, keine Technik
  grundstueck: {
    label: 'Unbebautes Grundstück',
    hatGeschosse: false,
    pflicht: ['aussen', 'grundstueck'],
    optional: ['anwesende', 'maengel'],
    hinweise: [
      'Topografie, Zuschnitt und Erschließungszustand dokumentieren',
      'Altlasten-Verdacht? (Vornutzung, Auffüllungen)',
    ],
  },

  // Eigentumswohnung: Trennung Sonder-/Gemeinschaftseigentum
  etw: {
    label: 'Eigentumswohnung',
    hatGeschosse: true,            // Geschoss-Tags = die Wohnung (Sondereigentum)
    geschosseSind: 'se',           // Geschoss-Tags als Sondereigentum markieren
    pflichtGE: ['aussen', 'gebaeude', 'dach', 'technik_heizung'], // Gemeinschaftseigentum
    pflichtSonstiges: ['maengel'],
    optional: ['anwesende', 'keller', 'garage'],
    hinweise: [
      'Sondereigentum (Wohnung) und Gemeinschaftseigentum getrennt erfassen',
      'Gemeinschaftseigentum: Dach, Fassade, Treppenhaus, zentrale Heizung',
      'WEG-Protokolle, Teilungserklärung und Hausgeld-/Rücklagenstand prüfen',
    ],
  },

  // Mehrfamilienhaus: ganzes Gebäude + Einheiten + Vermietungsstand
  mfh: {
    label: 'Mehrfamilienhaus',
    hatGeschosse: true,
    pflicht: ['aussen', 'grundstueck', 'gebaeude', 'dach', 'technik_heizung', 'technik_wasser'],
    pflichtSonstiges: ['garage', 'maengel'],
    optional: ['anwesende'],
    hinweise: [
      'Jede Wohneinheit einzeln erfassen (Lage, Größe, Zustand)',
      'Vermietungsstand und Mietniveau je Einheit notieren',
      'Gemeinschaftsflächen (Treppenhaus, Keller, Waschküche) dokumentieren',
    ],
  },

  // Einfamilienhaus / Doppelhaushälfte: Standardfall, alle Geschosse
  efh: {
    label: 'Einfamilienhaus',
    hatGeschosse: true,
    pflicht: ['aussen', 'grundstueck', 'gebaeude', 'dach', 'technik_heizung', 'technik_wasser'],
    pflichtSonstiges: ['garage', 'maengel'],
    optional: ['anwesende'],
    hinweise: [],
  },
  dhh: {
    label: 'Doppelhaushälfte',
    hatGeschosse: true,
    pflicht: ['aussen', 'grundstueck', 'gebaeude', 'dach', 'technik_heizung', 'technik_wasser'],
    pflichtSonstiges: ['garage', 'maengel'],
    optional: ['anwesende'],
    hinweise: [
      'Grenzbebauung / gemeinsame Trennwand zur Nachbarhälfte beachten',
    ],
  },
};

// Fallback-Profil für unbekannte Objekttypen (wie EFH ohne Spezial-Hinweise)
const RUNDGANG_PROFILE_DEFAULT = {
  label: 'Objekt',
  hatGeschosse: true,
  pflicht: ['aussen', 'grundstueck', 'gebaeude', 'dach', 'technik_heizung'],
  pflichtSonstiges: ['garage', 'maengel'],
  optional: ['anwesende'],
  hinweise: [],
};

// Profil für Nebengebäude (minimaler Rundgang, keine Geschosse)
const RUNDGANG_PROFILE_NEBENGEBAEUDE = {
  label: 'Nebengebäude',
  hatGeschosse: false,
  pflicht: ['aussen', 'gebaeude'],
  optional: ['anwesende', 'maengel'],
  hinweise: ['Nutzung, Bauweise und Zustand des Nebengebäudes dokumentieren'],
};

// Normalisiert objekttyp-Werte (DB speichert teils Label, teils Value)
function normObjekttyp(raw) {
  const t = (raw || '').toLowerCase().trim();
  if (!t) return '';
  // Label → value Mapping (falls die DB den Anzeigetext gespeichert hat)
  if (t.includes('eigentumswohnung') || t === 'etw') return 'etw';
  if (t.includes('mehrfamilien') || t === 'mfh') return 'mfh';
  if (t.includes('doppelhaus') || t.includes('reihenhaus') || t.startsWith('dhh')) return 'dhh';
  if (t.includes('grundstück') || t.includes('grundstueck') || t === 'grundstueck') return 'grundstueck';
  if (t.includes('einfamilien') || t === 'efh') return 'efh';
  if (t.includes('stellplatz') || t.includes('tg') || t === 'stellplatz') return 'stellplatz';
  if (t.includes('keller')) return 'keller';
  if (t.includes('gewerbe')) return 'gewerbe';
  return t;
}

// ── DIE zentrale Funktion ──
// Gibt für ein Bewertungsobjekt die zu erfassenden Bereiche zurück.
// Return: { profil, pflicht: [{id,label,eigentum}], optional: [...], hinweise: [...] }
function getRundgangBereiche(obj) {
  if (!obj) obj = {};
  const istNebengebaeude = obj.kategorie === 'nebengebaeude';
  const typ = normObjekttyp(obj.objekttyp);
  const profil = istNebengebaeude
    ? RUNDGANG_PROFILE_NEBENGEBAEUDE
    : (OBJEKT_RUNDGANG_PROFILE[typ] || RUNDGANG_PROFILE_DEFAULT);

  const labelFor = (id) => VL_KAPITEL_TAGS.find(t => t.id === id)?.label || id;
  const geschosseSet = new Set(obj.geschosse && obj.geschosse.length ? obj.geschosse : ['kg', 'eg', 'dg']);

  const pflicht = [];
  const optional = [];

  // Geschoss-Tags (gefiltert nach obj.geschosse), falls das Profil Geschosse hat
  const geschossTags = profil.hatGeschosse
    ? GESCHOSS_TAG_ORDER.filter(g => geschosseSet.has(g.flag)).map(g => g.id)
    : [];

  if (typ === 'etw' && !istNebengebaeude) {
    // ETW: Gemeinschaftseigentum + Sondereigentum (= Geschosse) getrennt
    for (const id of (profil.pflichtGE || [])) {
      pflicht.push({ id, label: labelFor(id), eigentum: 'ge' });
    }
    for (const id of geschossTags) {
      pflicht.push({ id, label: labelFor(id), eigentum: 'se' });
    }
    for (const id of (profil.pflichtSonstiges || [])) {
      pflicht.push({ id, label: labelFor(id), eigentum: null });
    }
    for (const id of (profil.optional || [])) {
      optional.push({ id, label: labelFor(id), eigentum: null });
    }
  } else {
    // Alle anderen: pflicht + geschosse + sonstiges
    for (const id of (profil.pflicht || [])) {
      pflicht.push({ id, label: labelFor(id), eigentum: null });
    }
    for (const id of geschossTags) {
      pflicht.push({ id, label: labelFor(id), eigentum: null });
    }
    for (const id of (profil.pflichtSonstiges || [])) {
      pflicht.push({ id, label: labelFor(id), eigentum: null });
    }
    for (const id of (profil.optional || [])) {
      optional.push({ id, label: labelFor(id), eigentum: null });
    }
  }

  // Duplikate entfernen (id-basiert), Reihenfolge wahren
  const seen = new Set();
  const dedupe = (arr) => arr.filter(x => { if (seen.has(x.id)) return false; seen.add(x.id); return true; });
  const pflichtClean = dedupe(pflicht);
  const optionalClean = dedupe(optional);

  return {
    profil,
    pflicht: pflichtClean,
    optional: optionalClean,
    hinweise: profil.hinweise || [],
  };
}


// ────────────────────────────────────────────────────────────────────
// ENTWURF-TAB: Kapitel-Struktur und Tag-Labels
// Spiegelt die Gutachten-Template-Reihenfolge für den Entwurf-Tab.
// ────────────────────────────────────────────────────────────────────

const ENTWURF_KAPITEL = [
  {
    id: 'lage', label: 'Lage',
    subkapitel: [
      { id: 'makrolage', label: 'Makrolage', aiTags: ['makrolage_1','makrolage_2','makrolage_3','makrolage_4','makrolage_5','makrolage_6'] },
      { id: 'demografie', label: 'Demografische Entwicklung', aiTags: ['demografie_einleitung'] },
      { id: 'mikrolage', label: 'Mikrolage', aiTags: ['kleinraeumige_lage','strasse_parkplatz','areal_umgebung','nahversorgung','verkehrsanbindung'] },
      { id: 'beurteilung_lage', label: 'Beurteilung', isBeurteilung: true, aiTags: ['beurteilung_lage','lageurteil'] },
    ],
  },
  {
    id: 'grundstueck', label: 'Grundstück',
    subkapitel: [
      { id: 'grundstuecksbeschreibung', label: 'Beschreibung', aiTags: ['grundstueckszuschnitt','topographie','hoehenlage','ausrichtung','angrenzungen','bebauung_grundstueck','zufahrt','zuwegung','einfriedung','freiflaechengestaltung','freiflaechengestaltung_2','freiflaechengestaltung_3'] },
      { id: 'umwelt', label: 'Umwelt und Immissionen', aiTags: ['altlastenauskunft','umwelteinfluesse','naturgefahren','immissionen'] },
      { id: 'beurteilung_grundstueck', label: 'Beurteilung', isBeurteilung: true, aiTags: ['beurteilung_grundstueck_1','beurteilung_grundstueck_2','beurteilung_grundstueck_3','beurteilung_grundstueck_4','beurteilung_grundstueck_5'] },
    ],
  },
  {
    id: 'recht', label: 'Rechtliche Gegebenheiten',
    subkapitel: [
      { id: 'grundbuch_einleitung', label: 'Grundbuch', aiTags: ['einleitung_wohnungsgrundbuch','einleitung_teileigentumsgrundbuch'] },
      { id: 'bauplanungsrecht', label: 'Bauplanungsrecht', aiTags: ['bauplanungsrecht','flaechennutzungsplan','satzungen'] },
      { id: 'erschliessung', label: 'Erschließung', aiTags: ['erschliessungsbeitraege','herstellungsbeitraege','grundstuecksentwaesserung'] },
      { id: 'recht_sonstiges', label: 'Sonstiges', aiTags: ['gebaeudeversicherung','denkmalschutz','gemeinschaftseigentum','nachtraege_teilungserklaerung','mietvertraege','zubehoer'] },
    ],
  },
  {
    id: 'gebaeude', label: 'Gebäude',
    subkapitel: [
      { id: 'allgemein', label: 'Allgemein', aiTags: ['wohnhaus_einleitung','bewertungseinheit_zuordnung','baujahre','gebaeudeart','hauseingang','vertikale_erschliessung'] },
      { id: 'bauteile', label: 'Konstruktiver Aufbau', aiTags: ['fassade','fenster_allgemein','dach','geschossdecken','heizungsanlage_allgemein'] },
      { id: 'kg', label: 'Kellergeschoss', aiTags: ['kg_raumaufteilung','kg_fussboeden','kg_waende','kg_decken','kg_tueren','kg_fenster','kg_heizungsanlage','kg_beheizung','kg_sonstiges'] },
      { id: 'eg', label: 'Erdgeschoss', aiTags: ['eg_raumaufteilung','eg_fussboeden','eg_waende','eg_decken','eg_tueren','eg_fenster','eg_beheizung','eg_sanitaer','eg_sonstiges'] },
      { id: 'dg', label: 'Dachgeschoss', aiTags: ['dg_raumaufteilung','dg_fussboeden','dg_waende','dg_decken','dg_tueren','dg_fenster','dg_beheizung','dg_sanitaer','dg_sonstiges'] },
      { id: 'sp', label: 'Spitzboden', aiTags: ['sp_zugang','sp_raumaufteilung','sp_fussboeden','sp_schraegen','sp_beheizung','sp_belichtung','sp_sonstiges'] },
      { id: 'garage', label: 'Garage', aiTags: ['garage_einleitung','garage_ausstattung'] },
      { id: 'sonstige_bauliche', label: 'Sonstige bauliche Besonderheiten', aiTags: ['energieausweis','schornsteinfeger'] },
      { id: 'beurteilung_gebaeude', label: 'Beurteilung', isBeurteilung: true, aiTags: ['beurteilung_gebaeude_1','beurteilung_gebaeude_2','beurteilung_gebaeude_3','beurteilung_gebaeude_4'] },
    ],
  },
  {
    id: 'verfahren', label: 'Wertermittlung',
    subkapitel: [
      { id: 'markt', label: 'Markt und Wirtschaft', aiTags: ['standortbewertung','markteinordnung','marktdynamik','marktzusammenfassung'] },
      { id: 'verfahrenswahl_sub', label: 'Verfahrenswahl', aiTags: ['gegenstand_wertermittlung','nutzungsperspektive','verfahrensbegruendung','verfahrenswahl'] },
      { id: 'bodenwert', label: 'Bodenwert', aiTags: ['bodenrichtwerte_beschreibung','grundstuecksdaten_bodenwert','erschliessungsstatus_bodenwert','lagebeurteilung_bodenwert','preisentwicklung_bodenwert','brw_heranziehung'] },
      { id: 'sachwert', label: 'Sachwert', aiTags: ['sachwert_modell','gnd_wohnhaus','gnd_garage','rnd_berechnung','rnd_modifikation','aussenanlagen_ansatz','besondere_bauteile','sachwertfaktor','sachwertfaktor_energetik','sachwertfaktor_preisentwicklung','sachwertfaktor_ergebnis','bogs_reparaturrueckstau'] },
      { id: 'lasten', label: 'Wert der Lasten', aiTags: ['lasten_einleitung','lasten_beschreibung','lasten_urkunde','lasten_lageplan','lasten_beurteilung','lasten_einschaetzung','lasten_ergebnis'] },
      { id: 'verkehrswert_sub', label: 'Verkehrswert', aiTags: ['verkehrswert_ableitung','verkehrswert_feststellung','schlussvermerk'] },
    ],
  },
];

const ENTWURF_TAG_LABELS = {
  // Lage
  makrolage_1:'Stadt/Gemeinde', makrolage_2:'Region/Nachbarstädte', makrolage_3:'ÖPNV', makrolage_4:'Versorgung',
  makrolage_5:'Soziale Infrastruktur', makrolage_6:'Freizeit/Naherholung',
  demografie_einleitung:'Einleitungssatz', kleinraeumige_lage:'Kleinräumige Lage', strasse_parkplatz:'Straße und Parkplatzsituation',
  areal_umgebung:'Areal und Umgebungsbebauung', nahversorgung:'Nahversorgung, Bildungseinrichtungen und medizinische Versorgung', verkehrsanbindung:'Verkehrsanbindung',
  beurteilung_lage:'Beurteilung', lageurteil:'Wohnlage-Einstufung',
  // Grundstück
  grundstueckszuschnitt:'Grundstückszuschnitt', topographie:'Grundstückstopographie', hoehenlage:'Höhenlage zur Straße', ausrichtung:'Grundstücksausrichtung',
  angrenzungen:'Angrenzungen/Erschließung', bebauung_grundstueck:'Bebauung', zufahrt:'Zufahrt/Zuwegung', zuwegung:'Zufahrt/Zuwegung',
  einfriedung:'Einfriedung', freiflaechengestaltung:'Freiflächengestaltung',
  freiflaechengestaltung_2:'Freiflächengestaltung', freiflaechengestaltung_3:'Freiflächengestaltung',
  altlastenauskunft:'Altlasten', umwelteinfluesse:'Umwelteinflüsse', naturgefahren:'Naturgefahren', immissionen:'Immissionen',
  beurteilung_grundstueck_1:'Form und Lage', beurteilung_grundstueck_2:'Zufahrt', beurteilung_grundstueck_3:'Freiflächen',
  beurteilung_grundstueck_4:'Altlasten', beurteilung_grundstueck_5:'Umwelt/Naturgefahren',
  // Recht
  einleitung_wohnungsgrundbuch:'Wohnungsgrundbuch', einleitung_teileigentumsgrundbuch:'Teileigentumsgrundbuch',
  bauplanungsrecht:'Bauplanungsrecht', flaechennutzungsplan:'Flächennutzungsplan', satzungen:'Satzungen',
  erschliessungsbeitraege:'Beitragsrechtlicher Zustand', herstellungsbeitraege:'Beitragsrechtlicher Zustand',
  grundstuecksentwaesserung:'Beitragsrechtlicher Zustand',
  gebaeudeversicherung:'Gebäudeversicherung', denkmalschutz:'Denkmalschutz',
  gemeinschaftseigentum:'Gemeinschaftseigentum', nachtraege_teilungserklaerung:'Nachträge TE',
  mietvertraege:'Miet-/Pachtverträge', zubehoer:'Zubehör',
  // Gebäude
  wohnhaus_einleitung:'Einleitung', bewertungseinheit_zuordnung:'Bewertungseinheit', baujahre:'Baujahre', gebaeudeart:'Gebäudeart',
  hauseingang:'Hauseingang', vertikale_erschliessung:'Vertikale Erschließung',
  fassade:'Fassade', fenster_allgemein:'Fenster (allgemein)', dach:'Dach', geschossdecken:'Geschossdecken',
  heizungsanlage_allgemein:'Heizungsanlage',
  // KG
  kg_raumaufteilung:'Raumaufteilung', kg_fussboeden:'Fußböden', kg_waende:'Wände',
  kg_decken:'Decken', kg_tueren:'Türen', kg_fenster:'Fenster', kg_heizungsanlage:'Heizungsanlage (Detail)',
  kg_beheizung:'Beheizung', kg_sonstiges:'Sonstiges',
  // EG
  eg_raumaufteilung:'Raumaufteilung', eg_fussboeden:'Fußböden', eg_waende:'Wände',
  eg_decken:'Decken', eg_tueren:'Türen', eg_fenster:'Fenster / Terrassentür', eg_beheizung:'Beheizung',
  eg_sanitaer:'Sanitärgegenstände', eg_sonstiges:'Sonstiges',
  // DG
  dg_raumaufteilung:'Raumaufteilung', dg_fussboeden:'Fußböden', dg_waende:'Wände / Drempel',
  dg_decken:'Schrägen / Decken', dg_tueren:'Türen', dg_fenster:'Fenster / Balkontür', dg_beheizung:'Beheizung',
  dg_sanitaer:'Sanitärgegenstände', dg_sonstiges:'Sonstiges',
  // Spitzboden
  sp_zugang:'Zugang', sp_raumaufteilung:'Raumaufteilung', sp_fussboeden:'Fußboden', sp_schraegen:'Dachschrägen / Dämmung',
  sp_beheizung:'Beheizung', sp_belichtung:'Belichtung', sp_sonstiges:'Sonstiges',
  // Garage
  garage_einleitung:'Garage (Einleitung)', garage_ausstattung:'Garage (Ausstattung)',
  // Sonstige bauliche Besonderheiten
  energieausweis:'Energieausweis', schornsteinfeger:'Schornsteinfeger',
  // Beurteilung Gebäude
  beurteilung_gebaeude_1:'Errichtung', beurteilung_gebaeude_2:'Modernisierungsstatus',
  beurteilung_gebaeude_3:'Energetik / Barrierefreiheit', beurteilung_gebaeude_4:'Gesamtzustand / Rückstau',
  // Verfahren
  standortbewertung:'Standortbewertung', markteinordnung:'Markteinordnung', marktdynamik:'Marktdynamik',
  marktzusammenfassung:'Zusammenfassung',
  gegenstand_wertermittlung:'Gegenstand', nutzungsperspektive:'Nutzungsperspektive',
  verfahrensbegruendung:'Begründung', verfahrenswahl:'Verfahrenswahl',
  // Bodenwert
  bodenrichtwerte_beschreibung:'Bodenrichtwerte', grundstuecksdaten_bodenwert:'Grundstücksdaten',
  erschliessungsstatus_bodenwert:'Erschließungsstatus', lagebeurteilung_bodenwert:'Lagebeurteilung',
  preisentwicklung_bodenwert:'Preisentwicklung', brw_heranziehung:'BRW-Heranziehung',
  // Sachwert
  sachwert_modell:'Modellverweis', gnd_wohnhaus:'GND Wohnhaus', gnd_garage:'GND Garage',
  rnd_berechnung:'RND-Berechnung', rnd_modifikation:'Modifizierte RND',
  aussenanlagen_ansatz:'Außenanlagen', besondere_bauteile:'Besondere Bauteile',
  sachwertfaktor:'Sachwertfaktor', sachwertfaktor_energetik:'SWF Energetik',
  sachwertfaktor_preisentwicklung:'SWF Preisentwicklung', sachwertfaktor_ergebnis:'SWF Ergebnis',
  // BoGs
  bogs_reparaturrueckstau:'Reparaturrückstau',
  // Lasten
  lasten_einleitung:'Einleitung', lasten_beschreibung:'Beschreibung', lasten_urkunde:'Urkunde',
  lasten_lageplan:'Lageplan', lasten_beurteilung:'Beurteilung', lasten_einschaetzung:'Einschätzung',
  lasten_ergebnis:'Ergebnis',
  // Verkehrswert
  verkehrswert_ableitung:'Ableitung', verkehrswert_feststellung:'Feststellung', schlussvermerk:'Schlussvermerk',
};

function entwurfTagLabel(tagId) {
  return ENTWURF_TAG_LABELS[tagId] || tagId.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}

// ── Multi-Objekt Helpers ──
// Composite Key für gutachten_tag_outputs:
//   Globale Tags  → "makrolage_1"
//   Objekt-Tags   → "eg_raumaufteilung::abc-123-uuid"
function outputKey(tagId, objektId) {
  return objektId ? `${tagId}::${objektId}` : tagId;
}

function getOutput(outputs, tagId, objektId) {
  // Composite-Key (tag::objektId) zuerst prüfen — für Tags die per Objekt gespeichert sind.
  // Fallback auf Plain-Key (tag) — für Tags die global gespeichert sind.
  // Hintergrund: Die Schema-Definitionen haben aktuell kein scope='objekt',
  // daher werden alle Tags global gespeichert. Das Frontend sucht Sektion-6-Tags
  // aber mit objektId → Composite-Key → findet nichts. Dieser Fallback löst das.
  if (objektId) {
    const composite = outputs[outputKey(tagId, objektId)];
    if (composite) return composite;
  }
  return outputs[tagId];
}

const ENTWURF_STATUS_CONFIG = {
  pending:    { label: 'Offen',        color: 'var(--text-tertiary)',  bg: 'var(--surface-light)',     dot: 'var(--border-medium)' },
  generating: { label: 'Generiert...', color: 'var(--vl-blue)',       bg: 'var(--surface-blue)',      dot: 'var(--vl-blue)' },
  generated:  { label: 'Generiert',    color: 'var(--success)',        bg: 'var(--success-bg)',        dot: 'var(--success)' },
  edited:     { label: 'Bearbeitet',   color: 'var(--vl-orange-dark)',bg: 'var(--vl-orange-bg)',      dot: 'var(--vl-orange)' },
  error:      { label: 'Fehler',       color: 'var(--danger)',         bg: 'rgba(220,38,38,0.08)',     dot: 'var(--danger)' },
};

function computeKapitelStats(subkapitel, outputs) {
  let total = 0, done = 0;
  for (const sub of subkapitel) {
    for (const tagId of sub.aiTags) {
      total++;
      const o = outputs[tagId];
      if (o && (o.status === 'generated' || o.status === 'edited')) done++;
    }
  }
  return { total, done, pct: total === 0 ? 0 : Math.round((done / total) * 100) };
}

// ────────────────────────────────────────────────────────────────────
// Signalphrasen für Live-Raum-Erkennung aus Web-Speech-Text
// ────────────────────────────────────────────────────────────────────
const RUNDGANG_SEGMENT_PATTERNS = [
  { regex: /\bich beginne (jetzt )?(im|mit dem|in der|auf dem) ([a-zäöü]+geschoss|keller|dachboden|dach|grundstück|außen)/i,
    priority: 100, captureGroup: 3 },
  { regex: /\b(kurz )?zum (gebäude|grundstück|außenbereich|dach)/i,
    priority: 95, captureGroup: 2 },
  { regex: /\b(wir|ich) befinden? (uns|mich) (jetzt |nun )?(im|in der|auf dem) ([a-zäöü]+)/i,
    priority: 90, captureGroup: 5 },
  { regex: /\b(dann )?geht es (jetzt |nun |weiter )?(in den|in die|ins|auf den) ([a-zäöü]+)/i,
    priority: 80, captureGroup: 4 },
  { regex: /\bhier (sind|haben) wir (den|die|das) ([a-zäöü]+)/i,
    priority: 70, captureGroup: 3 },
  { regex: /\b(kellergeschoss|kellerflur|kellerraum)\b/i, priority: 50, targetId: "keller" },
  { regex: /\b(erdgeschoss|eg[\s.,])\b/i, priority: 50, targetId: "eg" },
  { regex: /\b(1\.\s*og|erstes obergeschoss|erster stock)\b/i, priority: 50, targetId: "og1" },
  { regex: /\b(2\.\s*og|zweites obergeschoss)\b/i, priority: 50, targetId: "og2" },
  { regex: /\b(dachgeschoss|dachboden|spitzboden)\b/i, priority: 50, targetId: "dg" },
  { regex: /\b(auf dem dach|dachfläche|dacheindeckung)\b/i, priority: 50, targetId: "dach" },
  { regex: /\b(außenbereich|fassade|einfriedigung|grundstück|garten|zufahrt)\b/i, priority: 45, targetId: "aussen" },
  { regex: /\b(garage|carport|stellplatz)\b/i, priority: 50, targetId: "garage" },
  { regex: /\b(heizung|heizraum|heizungskeller|therme|brennwert|wärmepumpe|öltank|gastherme)\b/i, priority: 48, targetId: "technik_heizung" },
  { regex: /\b(sanitär|wasseranschluss|wasserzähler|hausanschluss wasser|installation wasser)\b/i, priority: 48, targetId: "technik_wasser" },
  { regex: /\b(mangel|mängel|schaden|schäden|riss|risse|feuchtigkeit|schimmel)\b/i, priority: 40, targetId: "maengel" },
];

const RUNDGANG_KEYWORD_TO_TAG = {
  "keller": "keller", "kellergeschoss": "keller", "kellerflur": "keller", "kellerraum": "keller", "untergeschoss": "keller",
  "erdgeschoss": "eg", "eg": "eg", "parterre": "eg",
  "obergeschoss": "og1", "og1": "og1", "1og": "og1", "erster": "og1", "ersten": "og1",
  "og2": "og2", "2og": "og2", "zweiter": "og2", "zweiten": "og2",
  "dachgeschoss": "dg", "dachboden": "dg", "spitzboden": "dg", "dg": "dg",
  "dach": "dach", "dachfläche": "dach", "dacheindeckung": "dach",
  "außenbereich": "aussen", "fassade": "aussen", "grundstück": "aussen", "garten": "aussen",
  "zufahrt": "aussen", "hauseingang": "aussen", "einfriedigung": "aussen",
  "garage": "garage", "carport": "garage", "stellplatz": "garage",
  "gebäude": "gebaeude",
};

const RUNDGANG_MARKER_PATTERNS = [
  /\bdas muss ich noch (schauen|prüfen|klären|verifizieren|nachsehen)/i,
  /\bden rest kann ich (dann )?erst/i,
  /\b(noch zu |muss noch )(klären|prüfen|ergänzen)/i,
  /\bdas ergänze ich\b/i,
  /\bhier muss ich nochmal/i,
  /\b(offener? punkt|offen)\b/i,
];

// Tags, die der Live-Detector überhaupt erkennen kann (aus den Patterns +
// Keyword-Map abgeleitet). Der Live-Nudge zeigt nur diese als "offen" an —
// Bereiche die der Detector nie trifft (z.B. 'gebaeude' als reiner Sammelbegriff)
// würden sonst dauerhaft offen erscheinen, auch wenn der SV sie bespricht.
const RUNDGANG_DETECTABLE_TAGS = new Set([
  'keller', 'eg', 'og1', 'og2', 'dg', 'dach', 'aussen', 'garage',
  'technik_heizung', 'technik_wasser', 'maengel', 'gebaeude',
]);

function detectSegmentBoundary(text, availableTags) {
  if (!text || text.length < 8) return null;
  const validTagIds = new Set(availableTags.map(t => t.id));
  const sorted = [...RUNDGANG_SEGMENT_PATTERNS].sort((a, b) => b.priority - a.priority);
  for (const pattern of sorted) {
    const match = pattern.regex.exec(text);
    if (!match) continue;
    let tagId = pattern.targetId;
    if (!tagId && pattern.captureGroup > 0) {
      const captured = (match[pattern.captureGroup] || "").toLowerCase().trim();
      tagId = RUNDGANG_KEYWORD_TO_TAG[captured];
      if (!tagId) {
        for (const [keyword, id] of Object.entries(RUNDGANG_KEYWORD_TO_TAG)) {
          if (captured.startsWith(keyword) || keyword.startsWith(captured)) {
            tagId = id;
            break;
          }
        }
      }
    }
    if (tagId && validTagIds.has(tagId)) {
      return { tagId, confidence: pattern.priority / 100, matchedPhrase: match[0] };
    }
  }
  return null;
}

function detectMarkerTrigger(text) {
  if (!text) return null;
  for (const regex of RUNDGANG_MARKER_PATTERNS) {
    const match = regex.exec(text);
    if (match) return { type: "auto_todo", phrase: match[0] };
  }
  return null;
}

// ────────────────────────────────────────────────────────────────────
// Objekt-Wechsel-Detection (V&L-spezifisch)
// Erkennt aus dem Live-Text, ob der Gutachter zu einem anderen
// Bewertungsobjekt wechselt. Beispiel: "jetzt zum Stellplatz" bei
// Müller-Gutachten mit ETW + Stellplatz + Keller.
// ────────────────────────────────────────────────────────────────────
function detectObjektSwitch(text, availableObjekte) {
  if (!text || text.length < 8 || !Array.isArray(availableObjekte) || availableObjekte.length < 2) {
    return null;
  }
  const lc = text.toLowerCase();
  // Normalize für Vergleich: Stellplatz, Keller, ETW, etc.
  for (const obj of availableObjekte) {
    const bezeichnung = (obj.bezeichnung || '').toLowerCase();
    if (!bezeichnung) continue;
    // Trigger-Phrases: "jetzt zum X", "nun beim X", "kommen zum X"
    const regex = new RegExp(
      `\\b(jetzt|nun|weiter|kommen|gehe|gehen|wechsle|wir sind) (zum|zur|ins|in den|in die|beim|bei der|bei dem) ${bezeichnung.split(/\s+/)[0]}`,
      'i'
    );
    if (regex.test(lc)) {
      return { objektId: obj.id, bezeichnung: obj.bezeichnung };
    }
    // Auch kurze Form: Objekttyp steht selbst im Text
    if (obj.objekttyp) {
      const typ = (obj.objekttyp || '').toLowerCase();
      if (typ && lc.includes(typ) && /\b(jetzt|nun|beim|im|bei der)\b/i.test(lc)) {
        return { objektId: obj.id, bezeichnung: obj.bezeichnung };
      }
    }
  }
  return null;
}

// ────────────────────────────────────────────────────────────────────
// dedupeAtOverlap — entfernt Overlap zwischen aufeinanderfolgenden
// Audio-Chunks (gpt-4o-transcribe transkribiert Chunk-Überlappung)
// ────────────────────────────────────────────────────────────────────
function dedupeAtOverlap(prevText, nextText) {
  if (!prevText || !nextText) return nextText || "";
  const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").replace(/[^a-zäöüß0-9 ]/g, "");
  const prevTail = norm(prevText.slice(-160));
  const nextHead = norm(nextText.slice(0, 400));
  if (!prevTail || !nextHead) return nextText;

  // Suche längste Suffix-Prefix-Übereinstimmung
  let bestMatchLen = 0;
  const minMatch = 15;
  for (let len = Math.min(prevTail.length, nextHead.length); len >= minMatch; len--) {
    const suffix = prevTail.slice(-len);
    if (nextHead.startsWith(suffix)) {
      bestMatchLen = len;
      break;
    }
  }
  if (bestMatchLen < minMatch) return nextText;

  // Finde die entsprechende Position im unnormalisierten nextText
  let normChars = 0;
  let cutPos = 0;
  for (let i = 0; i < nextText.length; i++) {
    const c = nextText[i].toLowerCase();
    if (/[a-zäöüß0-9 ]/.test(c) || /\s/.test(nextText[i])) normChars++;
    if (normChars >= bestMatchLen) {
      cutPos = i + 1;
      break;
    }
  }
  return nextText.slice(cutPos).trimStart();
}

// ────────────────────────────────────────────────────────────────────
// useLocalWhisper — Offline-Fallback via transformers.js (Stub für jetzt)
// In Stufe 4 initial nicht aktiviert, kommt mit Offline-Support später.
// ────────────────────────────────────────────────────────────────────
function useLocalWhisper() {
  return {
    isReady: false,
    isLoading: false,
    transcribe: async () => { throw new Error('Local Whisper nicht aktiviert'); },
  };
}

// ────────────────────────────────────────────────────────────────────
// useVoice — MediaRecorder + SpeechRecognition Dual-Channel
// Live-Transkript über SpeechRecognition, Audio-Blob für spätere
// Whisper-Korrektur via Worker. Portiert aus Augenschein-Alt-App.
// ────────────────────────────────────────────────────────────────────
async function requestWakeLock() {
  try {
    if ('wakeLock' in navigator) {
      return await navigator.wakeLock.request('screen');
    }
  } catch (e) {
    console.warn('[WakeLock] failed:', e.message);
  }
  return null;
}

function useVoice(localWhisper, getTranscribeContext, workerUrl, session, onChunkSaved) {
  const [isRec, setIsRec] = useState(false);
  const [text, setText] = useState('');
  const [interim, setInterim] = useState('');
  const [isTranscribing, setIsTranscribing] = useState(false);
  const [previewLive, setPreviewLive] = useState(false);
  const [micError, setMicError] = useState(null);
  const [correctionInfo, setCorrectionInfo] = useState(null);
  const [chunkCount, setChunkCount] = useState(0);
  const [elapsedMs, setElapsedMs] = useState(0);

  const ref = useRef(null);
  const wl = useRef(null);
  const mediaRef = useRef(null);
  const chunksRef = useRef([]);
  const allChunksRef = useRef([]);  // Alle Chunks für finalen Blob
  const stoppedByUser = useRef(false);
  const accumulatedText = useRef('');
  const restartCount = useRef(0);
  const lastAudioIdRef = useRef(null);
  const getContextRef = useRef(getTranscribeContext);
  getContextRef.current = getTranscribeContext;
  const onChunkSavedRef = useRef(onChunkSaved);
  onChunkSavedRef.current = onChunkSaved;
  const startTimeRef = useRef(0);
  const timerRef = useRef(null);
  const [wasInterrupted, setWasInterrupted] = useState(false);

  // ─── Interrupt-Schutz: visibilitychange + beforeunload ───
  useEffect(() => {
    const handleVisibility = () => {
      if (document.visibilityState === 'hidden' && mediaRef.current) {
        // App geht in Hintergrund (Anruf, WhatsApp, App-Wechsel, Bildschirmsperre)
        // Sofort aktuelle Chunks anfordern + sichern
        try {
          const { recorder } = mediaRef.current;
          if (recorder.state === 'recording') {
            recorder.requestData();
          }
        } catch {}
        // Zusätzlich: Safety-Blob sofort schreiben (requestData ist async)
        if (allChunksRef.current.length > 0 && onChunkSavedRef.current) {
          const safetyBlob = new Blob([...allChunksRef.current], {
            type: mediaRef.current?.mimeType || 'audio/webm',
          });
          onChunkSavedRef.current(safetyBlob, allChunksRef.current.length);
        }
      }
      if (document.visibilityState === 'visible' && mediaRef.current) {
        // App kommt zurück — prüfen ob MediaRecorder noch lebt
        const { recorder } = mediaRef.current;
        if (recorder.state === 'inactive' || recorder.state === 'paused') {
          if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
          setWasInterrupted(true);
          setIsRec(false);
          if (allChunksRef.current.length > 0 && onChunkSavedRef.current) {
            const safetyBlob = new Blob([...allChunksRef.current], {
              type: mediaRef.current.mimeType || 'audio/webm',
            });
            onChunkSavedRef.current(safetyBlob, allChunksRef.current.length);
          }
        }
        // Prüfen ob Audio-Track noch lebt
        try {
          const track = mediaRef.current.stream?.getAudioTracks()[0];
          if (track && track.readyState === 'ended') {
            if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
            setWasInterrupted(true);
            setIsRec(false);
          }
        } catch {}
      }
    };

    // beforeunload: Letzte Chance, Audio zu sichern bevor Tab geschlossen wird
    const handleBeforeUnload = () => {
      if (allChunksRef.current.length > 0 && onChunkSavedRef.current) {
        try {
          const safetyBlob = new Blob([...allChunksRef.current], {
            type: mediaRef.current?.mimeType || 'audio/webm',
          });
          onChunkSavedRef.current(safetyBlob, allChunksRef.current.length);
        } catch {}
      }
    };

    document.addEventListener('visibilitychange', handleVisibility);
    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => {
      document.removeEventListener('visibilitychange', handleVisibility);
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, []);

  const startSpeechRecognition = useCallback(() => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) return;
    try { ref.current?.abort(); } catch (e) {}
    const r = new SR();
    r.lang = 'de-DE';
    r.continuous = true;
    r.interimResults = true;
    r.onresult = (e) => {
      let f = '', i = '';
      for (let x = e.resultIndex; x < e.results.length; x++) {
        if (e.results[x].isFinal) f += e.results[x][0].transcript + ' ';
        else i += e.results[x][0].transcript;
      }
      if (f) accumulatedText.current = (accumulatedText.current + ' ' + f).trim();
      setText(accumulatedText.current);
      setInterim(i);
    };
    r.onerror = (e) => {
      if (e.error === 'no-speech' || e.error === 'aborted') return;
      setPreviewLive(false);
    };
    r.onend = () => {
      setInterim('');
      setPreviewLive(false);
      if (!stoppedByUser.current && restartCount.current < 50) {
        restartCount.current++;
        setTimeout(() => {
          if (!stoppedByUser.current) startSpeechRecognition();
        }, 300);
      }
    };
    ref.current = r;
    try { r.start(); setPreviewLive(true); } catch (e) { setPreviewLive(false); }
  }, []);

  const [noLivePreview, setNoLivePreview] = useState(false);

  const start = useCallback(async () => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    setNoLivePreview(!SR); // Safari: kein Live-Transkript, aber Aufnahme funktioniert

    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      setMicError(null);
      stoppedByUser.current = false;
      accumulatedText.current = '';
      restartCount.current = 0;
      lastAudioIdRef.current = null;
      wl.current = await requestWakeLock();

      const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus'
        : MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm'
        : MediaRecorder.isTypeSupported('audio/mp4;codecs=aac') ? 'audio/mp4;codecs=aac'
        : MediaRecorder.isTypeSupported('audio/mp4') ? 'audio/mp4'
        : undefined;
      const mr = new MediaRecorder(stream, mimeType ? { mimeType } : {});
      chunksRef.current = [];
      allChunksRef.current = [];
      let chunkIdx = 0;

      mr.ondataavailable = (e) => {
        if (e.data.size > 0) {
          chunksRef.current.push(e.data);
          allChunksRef.current.push(e.data);

          // Alle 30s: akkumulierte Chunks als Safety-Blob in IDB sichern
          if (chunksRef.current.length > 0 && onChunkSavedRef.current) {
            const safetyBlob = new Blob([...allChunksRef.current], { type: mimeType || 'audio/webm' });
            chunkIdx++;
            setChunkCount(chunkIdx);
            onChunkSavedRef.current(safetyBlob, chunkIdx);
          }
        }
      };

      // timeslice: emit ondataavailable alle 10 Sekunden (max 10s Verlust bei hartem Kill)
      mr.start(10000);

      // Fehler-Handler: Audio sichern wenn MediaRecorder abstürzt
      mr.onerror = (e) => {
        console.error('[Voice] MediaRecorder error:', e);
        if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
        if (allChunksRef.current.length > 0 && onChunkSavedRef.current) {
          const safetyBlob = new Blob([...allChunksRef.current], { type: mimeType || 'audio/webm' });
          onChunkSavedRef.current(safetyBlob, allChunksRef.current.length);
        }
        setWasInterrupted(true);
        setIsRec(false);
      };

      // Track-Ende: iOS nimmt das Mikrofon für Anruf/Siri/FaceTime
      const audioTrack = stream.getAudioTracks()[0];
      if (audioTrack) {
        audioTrack.onended = () => {
          console.warn('[Voice] Audio track ended (mic taken by OS)');
          if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
          // Sofort alles sichern was wir haben
          try { if (mr.state === 'recording') mr.requestData(); } catch {}
          if (allChunksRef.current.length > 0 && onChunkSavedRef.current) {
            const safetyBlob = new Blob([...allChunksRef.current], { type: mimeType || 'audio/webm' });
            onChunkSavedRef.current(safetyBlob, allChunksRef.current.length);
          }
          setWasInterrupted(true);
          setIsRec(false);
        };
        // Mute-Detection: Android kann Mic stumm schalten statt beenden
        audioTrack.onmute = () => {
          console.warn('[Voice] Audio track muted');
          try { if (mr.state === 'recording') mr.requestData(); } catch {}
        };
      }

      startTimeRef.current = Date.now();
      timerRef.current = setInterval(() => {
        setElapsedMs(Date.now() - startTimeRef.current);
      }, 1000);
      mediaRef.current = { recorder: mr, stream, mimeType };

      setText('');
      setInterim('');
      setIsRec(true);
      if (SR) startSpeechRecognition();
    } catch (e) {
      console.error('[Voice] getUserMedia failed:', e);
      setMicError(e.name === 'NotAllowedError' ? 'permission' : 'unavailable');
    }
  }, [startSpeechRecognition]);

  const stop = useCallback(async () => {
    stoppedByUser.current = true;
    setIsRec(false);
    if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    setElapsedMs(0);

    try { ref.current?.stop(); } catch (e) {}
    try { if (wl.current) { wl.current.release(); wl.current = null; } } catch {}

    const mediaContext = mediaRef.current;
    if (!mediaContext) return { text: accumulatedText.current, audioBlob: null };

    const { recorder, stream, mimeType } = mediaContext;

    // Recorder stoppen und auf letzten Chunk warten
    const audioBlob = await new Promise((resolve) => {
      recorder.onstop = () => {
        try { stream.getTracks().forEach(t => t.stop()); } catch {}
        const blob = new Blob(allChunksRef.current, { type: mimeType || 'audio/webm' });
        resolve(blob);
      };
      try { recorder.stop(); } catch { resolve(null); }
    });

    mediaRef.current = null;

    // Whisper-Upload (wenn Worker-URL + Session + Audio vorhanden)
    if (audioBlob && audioBlob.size > 1024 && workerUrl && session?.access_token) {
      setIsTranscribing(true);
      try {
        const ext = (mimeType || '').includes('webm') ? 'webm' : 'mp4';
        const filename = `voice-${Date.now()}.${ext}`;
        const formData = new FormData();
        formData.append('audio', audioBlob, filename);
        const ctx = getContextRef.current ? getContextRef.current() : {};
        formData.append('context', JSON.stringify(ctx));

        const res = await fetch(`${workerUrl}/api/transcribe`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${session.access_token}` },
          body: formData,
        });
        if (res.ok) {
          const data = await res.json();
          if (data.text) {
            setText(data.text);
            accumulatedText.current = data.text;
            setCorrectionInfo({
              corrected: data.corrected === true,
              rawText: data.raw_text || null,
            });
          }
        } else {
          console.warn('[Voice] Transcribe failed:', res.status);
        }
      } catch (e) {
        console.warn('[Voice] Transcribe error:', e);
      } finally {
        setIsTranscribing(false);
      }
    }

    return {
      text: accumulatedText.current,
      audioBlob,
      mimeType,
    };
  }, [workerUrl, session]);

  const reset = useCallback(() => {
    accumulatedText.current = '';
    setText('');
    setInterim('');
    setCorrectionInfo(null);
    setWasInterrupted(false);
    setChunkCount(0);
  }, []);

  // Resume nach Unterbrechung: neuer MediaRecorder, behält bisherigen Text + Audio
  const resume = useCallback(async () => {
    setWasInterrupted(false);
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus'
        : MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm'
        : MediaRecorder.isTypeSupported('audio/mp4;codecs=aac') ? 'audio/mp4;codecs=aac'
        : MediaRecorder.isTypeSupported('audio/mp4') ? 'audio/mp4'
        : undefined;
      const mr = new MediaRecorder(stream, mimeType ? { mimeType } : {});

      mr.ondataavailable = (e) => {
        if (e.data.size > 0) {
          chunksRef.current.push(e.data);
          allChunksRef.current.push(e.data);
          if (onChunkSavedRef.current) {
            const safetyBlob = new Blob([...allChunksRef.current], { type: mimeType || 'audio/webm' });
            onChunkSavedRef.current(safetyBlob, allChunksRef.current.length);
          }
        }
      };
      mr.onerror = (e) => {
        console.error('[Voice] MediaRecorder error on resume:', e);
        if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
        setWasInterrupted(true);
        setIsRec(false);
      };

      mr.start(10000);
      mediaRef.current = { recorder: mr, stream, mimeType };

      // Timer neu starten — startTimeRef bleibt vom Original-Start, Elapsed zählt korrekt weiter
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(() => {
        setElapsedMs(Date.now() - startTimeRef.current);
      }, 1000);

      // Track-Schutz auch bei Resume
      const audioTrack = stream.getAudioTracks()[0];
      if (audioTrack) {
        audioTrack.onended = () => {
          console.warn('[Voice] Audio track ended on resume');
          if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
          try { if (mr.state === 'recording') mr.requestData(); } catch {}
          setWasInterrupted(true);
          setIsRec(false);
        };
      }

      setIsRec(true);
      stoppedByUser.current = false;

      // SpeechRecognition neu starten
      const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
      if (SR) startSpeechRecognition();
    } catch (e) {
      console.error('[Voice] Resume failed:', e);
      setMicError(e.name === 'NotAllowedError' ? 'permission' : 'unavailable');
    }
  }, [startSpeechRecognition]);

  const clearMicError = useCallback(() => setMicError(null), []);

  return {
    isRec, text, interim, isTranscribing, previewLive, noLivePreview,
    start, stop, reset, resume, setText,
    lastAudioId: lastAudioIdRef, micError, clearMicError, correctionInfo,
    chunkCount, elapsedMs, wasInterrupted,
  };
}

// ────────────────────────────────────────────────────────────────────
// useRundgang — Rundgang-Hook mit Segment-Detection, Photo-Markern
// ────────────────────────────────────────────────────────────────────
function useRundgang({ availableTags, kapitelTags }) {
  const [segments, setSegments] = useState([]);
  const [markers, setMarkers] = useState([]);
  const [photoMarkers, setPhotoMarkers] = useState([]);
  const [currentTagId, setCurrentTagId] = useState(null);
  const [currentObjektId, setCurrentObjektId] = useState(null);
  const [coverage, setCoverage] = useState({});

  const startMsRef = useRef(null);
  const lastProcessedLenRef = useRef(0);
  const lastTagSwitchMsRef = useRef(0);
  const lastObjektSwitchMsRef = useRef(0);
  const currentTagIdRef = useRef(null);
  const currentObjektIdRef = useRef(null);
  const availableObjekteRef = useRef([]);

  const setAvailableObjekte = useCallback((objekte) => {
    availableObjekteRef.current = objekte || [];
  }, []);

  const reset = useCallback(() => {
    setSegments([]);
    setMarkers([]);
    setPhotoMarkers([]);
    setCurrentTagId(null);
    setCurrentObjektId(null);
    setCoverage({});
    startMsRef.current = null;
    lastProcessedLenRef.current = 0;
    lastTagSwitchMsRef.current = 0;
    lastObjektSwitchMsRef.current = 0;
    currentTagIdRef.current = null;
    currentObjektIdRef.current = null;
  }, []);

  const onStart = useCallback(() => {
    reset();
    startMsRef.current = Date.now();
  }, [reset]);

  const addPhotoMarker = useCallback((marker) => {
    setPhotoMarkers(prev => [...prev, marker]);
  }, []);

  const updatePhotoMarker = useCallback((id, patch) => {
    setPhotoMarkers(prev => prev.map(m => m.id === id ? { ...m, ...patch } : m));
  }, []);

  const getCurrentAudioPositionMs = useCallback(() => {
    return startMsRef.current ? (Date.now() - startMsRef.current) : 0;
  }, []);

  const onLiveText = useCallback((fullText) => {
    if (!startMsRef.current) return;
    const newContent = fullText.slice(lastProcessedLenRef.current);
    if (newContent.length < 10) return;
    lastProcessedLenRef.current = fullText.length;

    const now = Date.now();
    const elapsedMs = now - startMsRef.current;

    // Segment-Boundary (Kapitel-Wechsel)
    const boundary = detectSegmentBoundary(newContent, availableTags);
    if (boundary && boundary.tagId !== currentTagIdRef.current) {
      if (now - lastTagSwitchMsRef.current >= 5000) {
        const tagLabel = availableTags.find(t => t.id === boundary.tagId)?.label || boundary.tagId;
        lastTagSwitchMsRef.current = now;
        currentTagIdRef.current = boundary.tagId;
        setSegments(prev => {
          const closed = prev.length > 0
            ? prev.slice(0, -1).concat({ ...prev[prev.length - 1], endMs: elapsedMs })
            : prev;
          return closed.concat({
            tagId: boundary.tagId,
            label: tagLabel,
            startMs: elapsedMs,
            endMs: null,
            textStart: fullText.length,
            objektId: currentObjektIdRef.current,
          });
        });
        setCurrentTagId(boundary.tagId);
      }
    }

    // Objekt-Wechsel (V&L-spezifisch)
    const objektSwitch = detectObjektSwitch(newContent, availableObjekteRef.current);
    if (objektSwitch && objektSwitch.objektId !== currentObjektIdRef.current) {
      if (now - lastObjektSwitchMsRef.current >= 5000) {
        lastObjektSwitchMsRef.current = now;
        currentObjektIdRef.current = objektSwitch.objektId;
        setCurrentObjektId(objektSwitch.objektId);
      }
    }

    // Marker (Self-Notes)
    const markerTrigger = detectMarkerTrigger(newContent);
    if (markerTrigger) {
      setMarkers(prev => [...prev, {
        phrase: markerTrigger.phrase,
        timestampMs: elapsedMs,
        type: markerTrigger.type,
        auto: true,
      }]);
    }
  }, [availableTags]);

  return {
    segments, markers, photoMarkers, currentTagId, currentObjektId, coverage,
    reset, onStart, onLiveText, addPhotoMarker, updatePhotoMarker,
    getCurrentAudioPositionMs, setAvailableObjekte,
  };
}

// ────────────────────────────────────────────────────────────────────
// Foto-Bildkomprimierung (vor Upload) — Portiert aus Alt-App
// ────────────────────────────────────────────────────────────────────
function compressImage(file, maxW = 1200, q = 0.8) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const img = new Image();
      img.onload = () => {
        const scale = Math.min(1, maxW / img.width);
        const w = img.width * scale;
        const h = img.height * scale;
        const canvas = document.createElement('canvas');
        canvas.width = w;
        canvas.height = h;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, w, h);
        canvas.toBlob((blob) => {
          if (blob) resolve(blob);
          else reject(new Error('toBlob failed'));
        }, 'image/jpeg', q);
      };
      img.onerror = reject;
      img.src = e.target.result;
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

// ────────────────────────────────────────────────────────────────────
// Call /api/split-rundgang nach Rundgang-Ende
// Liefert Segment-Array und Todo-Array, sonst Live-Segmente als Fallback
// ────────────────────────────────────────────────────────────────────
async function splitRundgangMitKI(fullTranscript, liveSegments, markers, projectContext, session, workerUrl) {
  try {
    const res = await fetch(workerUrl + '/api/split-rundgang', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${session.access_token}`,
      },
      body: JSON.stringify({
        transcript: fullTranscript,
        live_segments: liveSegments,
        markers: markers,
        project_context: projectContext,
      }),
    });
    if (!res.ok) {
      // Worker liefert bei Fehlern { error, message } — die nützliche Meldung
      // durchreichen statt nur den Statuscode (z.B. "Transkript zu lang…").
      let serverMsg = 'Split API ' + res.status;
      try {
        const errData = await res.json();
        if (errData.message) serverMsg = errData.message;
        else if (errData.error) serverMsg = errData.error;
      } catch {}
      throw new Error(serverMsg);
    }
    const data = await res.json();
    return {
      ok: true,
      segments: data.segments || [],
      todos: data.todos || [],
    };
  } catch (e) {
    console.warn('[Rundgang] KI-Split fehlgeschlagen, Live-Fallback:', e.message);
    return {
      ok: false,
      error: e.message || String(e),
      segments: (liveSegments || []).map(s => ({
        tagId: s.tagId,
        label: s.label,
        text: s.text || '(Segment nicht lesbar)',
        isBeurteilung: false,
        objektId: s.objektId || null,
      })).filter(s => s.text && s.text.length > 5),
      todos: (markers || []).map(m => ({ text: m.phrase })),
    };
  }
}

// ══════════════════════════════════════════════════════════════════
// 3 · UI-BAUSTEINE
// ══════════════════════════════════════════════════════════════════

const Pill = ({ variant = '', children, style }) => (
  <span className={`pill ${variant ? 'pill-' + variant : ''}`} style={style}>{children}</span>
);

const ProgressRow = ({ label, value, total, variant = '' }) => {
  const pct = total === 0 ? 0 : Math.round((value / total) * 100);
  const displayValue = total === 100 ? `${value}%` : `${value}/${total}`;
  return (
    <div className="progress-row">
      <span className="progress-label">{label}</span>
      <div className="progress-bar">
        <div className={`progress-bar-fill ${variant}`} style={{ width: `${pct}%` }}></div>
      </div>
      <span className="progress-value">{displayValue}</span>
    </div>
  );
};

const PrototypeHint = ({ children }) => (
  <div className="prototype-hint-box">{children}</div>
);

// Kontextuelle Hilfe: "?" Icon mit Popover-Text
const HelpTip = ({ text, style: extraStyle }) => {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const close = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', close);
    return () => document.removeEventListener('mousedown', close);
  }, [open]);
  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex', ...extraStyle }}>
      <button type="button" onClick={(e) => { e.stopPropagation(); setOpen(v => !v); }}
        style={{
          background: 'none', border: '1px solid var(--border-light)', borderRadius: '50%',
          width: 18, height: 18, display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: 10, fontWeight: 700, color: 'var(--text-tertiary)', cursor: 'pointer',
          lineHeight: 1, padding: 0, flexShrink: 0,
        }}
        title="Hilfe">?</button>
      {open && (
        <div style={{
          position: 'absolute', bottom: '100%', left: '50%', transform: 'translateX(-50%)',
          marginBottom: 6, padding: '8px 12px', background: 'var(--surface)',
          border: '1px solid var(--border-light)', borderRadius: 'var(--radius-md)',
          boxShadow: '0 4px 12px rgba(0,0,0,0.1)', fontSize: 12, lineHeight: 1.5,
          color: 'var(--text-secondary)', width: 240, zIndex: 100, textAlign: 'left',
        }}>{text}</div>
      )}
    </span>
  );
};

// ──────────────────────────────────────────────────────────────────
// QueuePositionBadge — Zeigt "Dokument 1 / 2 · Beschluss" im Header
// des UploadModal an, wenn mehrere Dokumente sequenziell extrahiert
// werden. Nur sichtbar, wenn queueContext.total > 1.
// ──────────────────────────────────────────────────────────────────
const QueuePositionBadge = ({ queueContext }) => {
  if (!queueContext || queueContext.total <= 1) return null;
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      padding: '3px 10px',
      background: 'var(--vl-blue, #003B71)', color: '#fff',
      borderRadius: 999, fontSize: 11, fontWeight: 600,
      letterSpacing: '0.02em',
      whiteSpace: 'nowrap',
    }}>
      {queueContext.position} / {queueContext.total}
      {queueContext.typLabel && (
        <span style={{ opacity: 0.85, fontWeight: 500 }}>· {queueContext.typLabel}</span>
      )}
    </span>
  );
};

// ──────────────────────────────────────────────────────────────────
// EditableField — click-to-edit Inline-Editor
// Unterstützt text, date, number, textarea, select.
// Speichert on-blur oder Enter, bricht ab mit Escape.
// onSave: async (newValue) => void — wirft bei Fehler, Feld bleibt offen.
// ──────────────────────────────────────────────────────────────────
const EditableField = ({
  value,
  onSave,
  type = 'text',           // 'text' | 'textarea' | 'date' | 'number' | 'select'
  options = null,          // für type='select': [{ value, label }, ...]
  placeholder = '—',
  display = null,          // optional: vorformatierter Anzeige-Content
  format = null,           // optional: (v) => anzeige-string
  style = {},
  inputStyle = {},
  disabled = false,
  openSignal = 0,          // ändert sich → Feld öffnet zum Bearbeiten (Sprung/Tastatur)
  onEnterNext = null,      // nach erfolgreichem Enter-Speichern aufgerufen (nächste Lücke)
}) => {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(value ?? '');
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  const [justSaved, setJustSaved] = useState(false);
  const savedTimerRef = useRef(null);
  useEffect(() => () => { if (savedTimerRef.current) clearTimeout(savedTimerRef.current); }, []);
  const inputRef = useRef(null);
  const [dropOpen, setDropOpen] = useState(false);
  const dropRef = useRef(null);

  // Close dropdown on outside click (includes portal-rendered dropdown)
  const portalDropRef = useRef(null);
  useEffect(() => {
    if (!dropOpen) return;
    const close = (e) => {
      if (dropRef.current && !dropRef.current.contains(e.target)
        && (!portalDropRef.current || !portalDropRef.current.contains(e.target))) {
        setDropOpen(false);
      }
    };
    document.addEventListener('mousedown', close);
    return () => document.removeEventListener('mousedown', close);
  }, [dropOpen]);

  useEffect(() => { if (!editing) { setDraft(value ?? ''); setDropOpen(false); } }, [value, editing]);
  useEffect(() => {
    if (editing && inputRef.current) {
      inputRef.current.focus();
      if (type === 'text' || type === 'textarea' || type === 'number') {
        inputRef.current.select?.();
      }
    }
  }, [editing, type]);

  // Von außen geöffnet (Sprung aus „Noch offen" / Tastatur-Weiterspringen)
  const openSignalRef = useRef(0);
  useEffect(() => {
    if (openSignal && openSignal !== openSignalRef.current) {
      openSignalRef.current = openSignal;
      if (!disabled) { setError(null); setEditing(true); }
    }
  }, [openSignal, disabled]);

  const commit = async (overrideValue, viaEnter = false) => {
    const raw = overrideValue !== undefined ? overrideValue : draft;
    const normalized = typeof raw === 'string' ? raw.trim() : raw;
    const next = normalized === '' ? null : normalized;
    if ((value ?? null) === next) {
      setEditing(false);
      setError(null);
      if (viaEnter && onEnterNext) onEnterNext();
      return;
    }
    setSaving(true);
    setError(null);
    try {
      await onSave(next);
      setEditing(false);
      setJustSaved(true);
      if (savedTimerRef.current) clearTimeout(savedTimerRef.current);
      savedTimerRef.current = setTimeout(() => setJustSaved(false), 1300);
      if (viaEnter && onEnterNext) onEnterNext();
    } catch (err) {
      setError(err.message || String(err));
    } finally {
      setSaving(false);
    }
  };

  const cancel = () => {
    setDraft(value ?? '');
    setEditing(false);
    setError(null);
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Escape') { e.preventDefault(); cancel(); }
    else if (e.key === 'Enter' && type !== 'textarea') { e.preventDefault(); commit(undefined, true); }
    else if (e.key === 'Enter' && type === 'textarea' && (e.metaKey || e.ctrlKey)) {
      e.preventDefault(); commit(undefined, true);
    }
  };

  if (!editing) {
    const rendered = display != null
      ? display
      : (value == null || value === ''
          ? <span style={{ color: 'var(--text-tertiary)' }}>{placeholder}</span>
          : (format ? format(value) : String(value)));
    return (
      <span
        className={justSaved ? 'flash-success' : undefined}
        onClick={() => !disabled && setEditing(true)}
        title={disabled ? '' : 'Klicken zum Bearbeiten'}
        style={{
          display: 'inline-block',
          minWidth: 40,
          padding: '2px 4px',
          marginLeft: -4,
          borderRadius: 'var(--radius-sm)',
          cursor: disabled ? 'default' : 'text',
          transition: 'background 0.1s',
          ...style,
        }}
        onMouseEnter={(e) => { if (!disabled) e.currentTarget.style.background = 'var(--surface-light)'; }}
        onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
      >
        {rendered}
      </span>
    );
  }

  const commonInputStyle = {
    width: '100%',
    padding: '4px 8px',
    fontSize: 'inherit',
    fontFamily: 'inherit',
    color: 'var(--text-primary)',
    background: 'var(--surface)',
    border: `1px solid ${error ? 'var(--danger)' : 'var(--vl-blue-light)'}`,
    borderRadius: 'var(--radius-sm)',
    outline: 'none',
    ...inputStyle,
  };

  let input;
  if (type === 'textarea') {
    input = (
      <textarea
        ref={inputRef} value={draft}
        onChange={(e) => setDraft(e.target.value)}
        onBlur={() => commit()} onKeyDown={handleKeyDown} disabled={saving}
        rows={3}
        style={{ ...commonInputStyle, resize: 'vertical', minHeight: 60 }}
      />
    );
  } else if (type === 'select' && options) {
    const selectedLabel = options.find(o => o.value === (draft ?? ''))?.label || '— leer —';
    const isMobileView = typeof window !== 'undefined' && window.innerWidth < 768;
    // Position für Portal-Dropdown berechnen
    let portalStyle = null;
    if (dropOpen && inputRef.current) {
      if (isMobileView) {
        // Mobile: Bottom-Sheet
        portalStyle = {
          position: 'fixed', bottom: 0, left: 0, right: 0,
          background: 'var(--surface)', borderTop: '1px solid var(--border-light)',
          borderRadius: 'var(--radius-lg) var(--radius-lg) 0 0',
          boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
          zIndex: 9999, maxHeight: '60vh', overflowY: 'auto',
          WebkitOverflowScrolling: 'touch',
          paddingBottom: 'env(safe-area-inset-bottom, 16px)',
        };
      } else {
        const rect = inputRef.current.getBoundingClientRect();
        const spaceBelow = window.innerHeight - rect.bottom;
        const openUp = spaceBelow < 280 && rect.top > spaceBelow;
        portalStyle = {
          position: 'fixed',
          left: rect.left,
          ...(openUp
            ? { bottom: window.innerHeight - rect.top + 4 }
            : { top: rect.bottom + 4 }),
          minWidth: Math.max(rect.width, 280), width: 'max-content',
          maxWidth: 'min(400px, 90vw)',
          background: 'var(--surface)', border: '1px solid var(--border-light)',
          borderRadius: 'var(--radius-md)', boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
          zIndex: 9999, maxHeight: 280, overflowY: 'auto',
          WebkitOverflowScrolling: 'touch',
        };
      }
    }
    input = (
      <div ref={dropRef} style={{ position: 'relative', width: '100%' }}>
        <button
          ref={inputRef}
          type="button"
          onClick={() => setDropOpen(!dropOpen)}
          onKeyDown={handleKeyDown}
          disabled={saving}
          style={{
            ...commonInputStyle,
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            cursor: 'pointer', textAlign: 'left', gap: 8,
            minHeight: 36,
          }}
        >
          <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{selectedLabel}</span>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ flexShrink: 0, opacity: 0.4, transform: dropOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }}>
            <polyline points="6 9 12 15 18 9"/>
          </svg>
        </button>
        {dropOpen && portalStyle && ReactDOM.createPortal(
          <div ref={portalDropRef} style={portalStyle} onMouseDown={e => e.stopPropagation()}>
            {isMobileView && (
              <div style={{ padding: '14px 16px 8px', borderBottom: '1px solid var(--border-light)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>Auswahl</span>
                <button onClick={() => setDropOpen(false)} style={{ background: 'none', border: 'none', fontSize: 18, color: 'var(--text-tertiary)', cursor: 'pointer', padding: '4px 8px' }}>×</button>
              </div>
            )}
            {[{ value: '', label: '— leer —' }, ...options].map(o => (
              <div
                key={o.value}
                onClick={() => { setDraft(o.value); setDropOpen(false); commit(o.value); }}
                style={{
                  padding: isMobileView ? '16px 20px' : '10px 16px',
                  fontSize: 14, cursor: 'pointer',
                  background: o.value === (draft ?? '') ? 'var(--surface-light)' : 'transparent',
                  fontWeight: o.value === (draft ?? '') ? 600 : 400,
                  color: o.value === '' ? 'var(--text-tertiary)' : 'var(--text-primary)',
                  borderBottom: isMobileView ? '1px solid var(--border-light)' : 'none',
                }}
                onMouseEnter={e => { e.currentTarget.style.background = 'var(--surface-light)'; }}
                onMouseLeave={e => { e.currentTarget.style.background = o.value === (draft ?? '') ? 'var(--surface-light)' : 'transparent'; }}
              >
                {o.value === (draft ?? '') && <span style={{ marginRight: 8 }}>✓</span>}
                {o.label}
              </div>
            ))}
          </div>,
          document.body
        )}
        {/* Mobile: Backdrop */}
        {dropOpen && isMobileView && ReactDOM.createPortal(
          <div onClick={() => setDropOpen(false)} style={{
            position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.3)', zIndex: 9998,
          }} />,
          document.body
        )}
      </div>
    );
  } else {
    input = (
      <input
        ref={inputRef} type={type}
        value={draft ?? ''}
        onChange={(e) => setDraft(e.target.value)}
        onBlur={() => commit()} onKeyDown={handleKeyDown} disabled={saving}
        style={commonInputStyle}
      />
    );
  }

  return (
    <span style={{ display: 'inline-block', width: '100%' }}>
      {input}
      {error && (
        <div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 2 }}>{error}</div>
      )}
    </span>
  );
};

// ──────────────────────────────────────────────────────────────────
// FeldHistoryPopover — Modal-Overlay mit Änderungs-Timeline eines Feldes
// Liest /api/feld-history, zeigt rückwärts chronologisch: wer hat wann
// was geändert, aus welcher Quelle (KI-Extraktion, manueller Edit,
// Konfliktauflösung). Revisionssichere Darstellung, alle Zeiten in DE-
// Format, mit Dokument-Link wenn change_source='extraction'.
// ──────────────────────────────────────────────────────────────────
const CHANGE_SOURCE_LABEL = {
  extraction:       'KI-Extraktion',
  conflict_resolve: 'Konflikt aufgelöst',
  manual_edit:      'Manuell geändert',
  initial:          'Initial angelegt',
  system:           'System',
};

const CHANGE_SOURCE_COLOR = {
  extraction:       'var(--success)',
  conflict_resolve: 'var(--warning)',
  manual_edit:      'var(--vl-blue, #003B71)',
  initial:          'var(--text-tertiary)',
  system:           'var(--text-tertiary)',
};

function formatDateTime(iso) {
  if (!iso) return '';
  try {
    const d = new Date(iso);
    const pad = (n) => String(n).padStart(2, '0');
    return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  } catch { return iso; }
}

function formatHistoryValue(v) {
  if (v === null || v === undefined) return <em style={{ color: 'var(--text-tertiary)' }}>(leer)</em>;
  if (typeof v === 'object') {
    // _record-Einträge bei Beteiligten: kompakte Darstellung
    const keys = Object.keys(v);
    if (keys.length === 0) return <em style={{ color: 'var(--text-tertiary)' }}>(leer)</em>;
    return (
      <span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
        {keys.slice(0, 3).map(k => `${k}: ${v[k] ?? '—'}`).join(', ')}
        {keys.length > 3 ? ', …' : ''}
      </span>
    );
  }
  const s = String(v);
  return s.length > 120 ? s.substring(0, 117) + '…' : s;
}

const FeldHistoryPopover = ({
  projektId, tabelle, rowId, feld, session, workerUrl,
  title, onClose, onOpenDocument,
}) => {
  const [entries, setEntries] = useState(null);
  const [err, setErr] = useState(null);

  useEffect(() => {
    let active = true;
    ladeFeldHistory({ projektId, tabelle, rowId, feld, session, workerUrl })
      .then(rows => { if (active) setEntries(rows); })
      .catch(e => { if (active) setErr(e.message || String(e)); });
    return () => { active = false; };
  }, [projektId, tabelle, rowId, feld, session, workerUrl]);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 640 }}>
        <div className="modal-header">
          <div>
            <div className="modal-title">Änderungs-Historie</div>
            <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 }}>
              {title || `${tabelle} · ${feld}`}
            </div>
          </div>
          <button className="modal-close" onClick={onClose}>×</button>
        </div>
        <div className="modal-body" style={{ padding: 'var(--space-4) var(--space-5)', maxHeight: '60vh', overflowY: 'auto' }}>
          {err && (
            <div style={{
              background: 'var(--danger-bg)', color: 'var(--danger)',
              padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
              fontSize: 13,
            }}>
              Historie konnte nicht geladen werden: {err}
            </div>
          )}
          {!err && entries === null && (
            <div style={{ padding: 'var(--space-4)', color: 'var(--text-tertiary)', fontSize: 13 }}>
              Lade…
            </div>
          )}
          {!err && entries && entries.length === 0 && (
            <div style={{
              padding: 'var(--space-4)', color: 'var(--text-tertiary)',
              fontSize: 13, textAlign: 'center',
            }}>
              Keine Änderungen protokolliert. Dieser Wert wurde entweder
              vor Aktivierung der Audit-Historie gesetzt oder ist unverändert.
            </div>
          )}
          {!err && entries && entries.length > 0 && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
              {entries.map((e, idx) => (
                <div key={e.id} style={{
                  borderLeft: `3px solid ${CHANGE_SOURCE_COLOR[e.change_source] || 'var(--border-medium)'}`,
                  paddingLeft: 'var(--space-3)',
                  paddingBottom: idx < entries.length - 1 ? 'var(--space-2)' : 0,
                }}>
                  <div style={{
                    display: 'flex', alignItems: 'center', gap: 'var(--space-2)',
                    flexWrap: 'wrap', marginBottom: 4,
                  }}>
                    <span style={{
                      fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
                      letterSpacing: '0.06em',
                      color: CHANGE_SOURCE_COLOR[e.change_source] || 'var(--text-secondary)',
                    }}>
                      {CHANGE_SOURCE_LABEL[e.change_source] || e.change_source}
                    </span>
                    <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                      {formatDateTime(e.changed_at)}
                    </span>
                    {(e.changed_by_name || e.changed_by_email) && (
                      <span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
                        · {e.changed_by_name || e.changed_by_email}
                      </span>
                    )}
                  </div>
                  <div style={{
                    display: 'grid', gridTemplateColumns: '60px 1fr',
                    gap: 6, fontSize: 13,
                  }}>
                    <span style={{ color: 'var(--text-tertiary)' }}>Vorher</span>
                    <span>{formatHistoryValue(e.old_value)}</span>
                    <span style={{ color: 'var(--text-tertiary)' }}>Nachher</span>
                    <span style={{ fontWeight: 500 }}>{formatHistoryValue(e.new_value)}</span>
                  </div>
                  {e.dokument_id && onOpenDocument && (
                    <button
                      type="button"
                      onClick={() => onOpenDocument(e.dokument_id)}
                      style={{
                        marginTop: 6,
                        background: 'none', border: 'none',
                        color: 'var(--vl-blue-light, #1E5A8E)',
                        fontSize: 12, cursor: 'pointer', padding: 0,
                        textDecoration: 'underline',
                      }}
                    >
                      Quelldokument öffnen: {e.dokument_titel || 'Dokument'}
                    </button>
                  )}
                  {e.change_reason && (
                    <div style={{
                      marginTop: 4, fontSize: 12, color: 'var(--text-secondary)',
                      fontStyle: 'italic',
                    }}>
                      Grund: {e.change_reason}
                    </div>
                  )}
                </div>
              ))}
            </div>
          )}
        </div>
        <div className="modal-footer">
          <button className="btn btn-ghost" onClick={onClose}>Schließen</button>
        </div>
      </div>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────────
// FeldHistoryButton — kleiner Icon-Button, der den Popover öffnet
// Kann eigenständig überall neben einem Feld verwendet werden, auch
// wenn keine Herkunft existiert (z.B. für manuell eingetragene Werte).
// Props: projektId, tabelle, rowId, feld, session, workerUrl, title,
//        onOpenDocument (optional, zum Öffnen von Quell-PDFs)
// ──────────────────────────────────────────────────────────────────
const FeldHistoryButton = ({
  projektId, tabelle, rowId, feld, session, workerUrl,
  title, onOpenDocument, size = 'sm',
}) => {
  const [open, setOpen] = useState(false);
  if (!projektId || !tabelle || !rowId || !feld) return null;
  return (
    <>
      <button
        type="button"
        onClick={(e) => { e.stopPropagation(); e.preventDefault(); setOpen(true); }}
        title="Änderungs-Historie anzeigen"
        style={{
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          width: size === 'sm' ? 20 : 24,
          height: size === 'sm' ? 20 : 24,
          padding: 0,
          background: 'transparent',
          border: '1px solid var(--border-medium)',
          borderRadius: 999,
          color: 'var(--text-tertiary)',
          cursor: 'pointer',
          fontSize: size === 'sm' ? 11 : 13,
          lineHeight: 1,
          transition: 'border-color 0.12s, color 0.12s, background 0.12s',
        }}
        onMouseEnter={(e) => {
          e.currentTarget.style.borderColor = 'var(--vl-blue-light)';
          e.currentTarget.style.color = 'var(--vl-blue)';
        }}
        onMouseLeave={(e) => {
          e.currentTarget.style.borderColor = 'var(--border-medium)';
          e.currentTarget.style.color = 'var(--text-tertiary)';
        }}
      >
        <svg width={size === 'sm' ? 11 : 13} height={size === 'sm' ? 11 : 13}
             viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
          <circle cx="12" cy="12" r="10"/>
          <polyline points="12 6 12 12 16 14"/>
        </svg>
      </button>
      {open && (
        <FeldHistoryPopover
          projektId={projektId}
          tabelle={tabelle}
          rowId={rowId}
          feld={feld}
          session={session}
          workerUrl={workerUrl}
          title={title}
          onClose={() => setOpen(false)}
          onOpenDocument={onOpenDocument}
        />
      )}
    </>
  );
};

// ──────────────────────────────────────────────────────────────────
// ProvenanceBadge — zeigt "aus Dokument X" bei extrahierten Feldern
// Klick öffnet das Quelldokument in neuem Tab.
// ──────────────────────────────────────────────────────────────────
const ProvenanceBadge = ({ herkunft, onOpen, onOpenInline, size = 'sm' }) => {
  if (!herkunft?.dokument_id) return null;
  const titel = herkunft.titel || 'Dokument';
  const label = titel.length > 25 ? titel.substring(0, 22) + '…' : titel;

  // onOpenInline hat Vorrang: inline-Viewer statt neuer Tab
  const handleClick = (e) => {
    e.stopPropagation();
    e.preventDefault();
    if (onOpenInline) onOpenInline(herkunft.dokument_id);
    else if (onOpen) onOpen(herkunft.dokument_id);
  };

  return (
    <button
      type="button"
      className="provenance-badge"
      onClick={handleClick}
      title={`Aus: ${titel} — Klicken zum Anzeigen`}
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        gap: 3,
        padding: size === 'sm' ? '1px 6px' : '2px 8px',
        background: 'var(--success-bg)',
        color: 'var(--success)',
        border: '1px solid transparent',
        borderRadius: 'var(--radius-sm)',
        fontSize: size === 'sm' ? 10 : 11,
        fontWeight: 600,
        letterSpacing: '0.03em',
        cursor: 'pointer',
        transition: 'border-color 0.12s, background 0.12s',
        whiteSpace: 'nowrap',
        maxWidth: '100%',
        overflow: 'hidden',
        textOverflow: 'ellipsis',
      }}
      onMouseEnter={(e) => {
        e.currentTarget.style.borderColor = 'var(--success)';
      }}
      onMouseLeave={(e) => {
        e.currentTarget.style.borderColor = 'transparent';
      }}
    >
      <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ flexShrink: 0 }}>
        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
        <polyline points="14 2 14 8 20 8"/>
      </svg>
      <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</span>
    </button>
  );
};

// ══════════════════════════════════════════════════════════════════
// 4.1 · VIEW: PROJEKTLISTE
// Liest Projekte aus Supabase via ladeProjektliste()
// ══════════════════════════════════════════════════════════════════

// ──────────────────────────────────────────────────────────────────
// Helper: Frist-Info mit Farb-Code (basierend auf Restdays)
// ──────────────────────────────────────────────────────────────────
// Zentrale Dringlichkeits-Farbskala — EINE Wahrheit für Frist-Pills, Zeitstrahl, KPIs, Kalender.
// überfällig = rot · ≤ 7 Tage = amber · ≤ 21 = grün · sonst neutral.
function fristFarbe(restTage) {
  if (restTage == null) return { fg: 'var(--text-tertiary)', bg: 'var(--surface-light)', border: 'var(--border-medium)' };
  if (restTage < 0)     return { fg: 'var(--danger)',        bg: 'var(--danger-bg)',     border: 'var(--danger)' };
  if (restTage <= 7)    return { fg: 'var(--warning)',       bg: 'var(--warning-bg)',    border: 'var(--warning)' };
  if (restTage <= 21)   return { fg: 'var(--success)',       bg: 'var(--success-bg)',    border: 'var(--success)' };
  return { fg: 'var(--text-secondary)', bg: 'var(--surface-light)', border: 'var(--border-medium)' };
}

function getFristInfo(fristRestDays, fristDatum) {
  if (fristRestDays == null || !fristDatum) return null;
  const diff = fristRestDays;
  const label = diff < 0
    ? `${Math.abs(diff)} Tage überfällig`
    : diff === 0 ? 'Heute'
    : diff === 1 ? 'Morgen'
    : `${diff} Tage`;
  const f = fristFarbe(diff);
  return { diff, label, color: f.fg, bg: f.bg, datum: fristDatum };
}

// ──────────────────────────────────────────────────────────────────
// Helper: Dokumenten-Fristen-Status (für Unterlagen-Tab Badges)
// ──────────────────────────────────────────────────────────────────
function getDokFristStatus(dok) {
  if (!dok.angefragt_am) return null;
  if (dok.eingetroffen_am) return { key: 'eingetroffen', label: 'Eingetroffen', color: 'var(--success, #16a34a)', bg: 'rgba(22,163,74,0.08)' };
  if (dok.frist_bis) {
    const days = daysUntil(dok.frist_bis);
    if (days < 0) return { key: 'ueberfaellig', label: `${Math.abs(days)}d überfällig`, color: 'var(--danger)', bg: 'var(--danger-bg, rgba(220,38,38,0.06))' };
    if (days <= 7) return { key: 'frist_nah', label: `Frist in ${days}d`, color: '#F59E0B', bg: 'rgba(245,158,11,0.08)' };
  }
  return { key: 'angefragt', label: 'Angefragt', color: 'var(--vl-blue)', bg: 'rgba(10,37,64,0.06)' };
}

// Status eines Dokuments fürs Unterlagen-Cockpit/Filter:
// vorhanden = echte Datei da; offene Anfrage = angefragt, noch nicht eingetroffen.
function istDokVorhanden(d) { return d.status !== 'angefragt' || !!d.eingetroffen_am; }
function istDokOffeneAnfrage(d) { return d.status === 'angefragt' && !!d.angefragt_am && !d.eingetroffen_am; }

// Dokumenttypen, für die Anfrage-Anschreiben existieren
const ANSCHREIBEN_DOC_TYPES = new Set([
  'grundbuchauszug', 'teilungserklaerung',
  'baurechtsauskunft', 'bebauungsplan', 'bodenrichtwert',
  'erschliessung_strasse', 'erschliessung_wasser', 'erschliessung_kanal',
  'baulasten', 'altlasten', 'denkmalschutz', 'lagekarte',
]);

// ──────────────────────────────────────────────────────────────────
// ProjektKpiTile — klickbare KPI-Kachel
// ──────────────────────────────────────────────────────────────────
// ──────────────────────────────────────────────────────────────────
// Wochen-Kalender — zeigt Ortstermine und Fristen auf einer Woche
// ──────────────────────────────────────────────────────────────────
const TAGE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];

const WochenKalender = ({ projekte, onOpen }) => {
  const [weekOffset, setWeekOffset] = useState(0);
  const [calView, setCalView] = useState('woche'); // 'woche' | 'monat'
  const [monthOffset, setMonthOffset] = useState(0);

  const montag = useMemo(() => {
    const d = new Date();
    d.setHours(0, 0, 0, 0);
    const day = d.getDay();
    const diff = day === 0 ? -6 : 1 - day;
    d.setDate(d.getDate() + diff + weekOffset * 7);
    return d;
  }, [weekOffset]);

  const tage = useMemo(() => {
    return Array.from({ length: 7 }, (_, i) => {
      const d = new Date(montag);
      d.setDate(d.getDate() + i);
      return d;
    });
  }, [montag]);

  const today = useMemo(() => {
    const d = new Date(); d.setHours(0, 0, 0, 0); return d.toISOString().split('T')[0];
  }, []);

  const events = useMemo(() => {
    const map = {};
    for (const t of tage) {
      map[t.toISOString().split('T')[0]] = [];
    }
    for (const p of (projekte || [])) {
      if (p.ortsterminDatum && map[p.ortsterminDatum]) {
        map[p.ortsterminDatum].push({ type: 'ortstermin', name: p.name, akte: p.akte, id: p.id, standort: p.standort });
      }
      if (p.abgabeExtern && map[p.abgabeExtern]) {
        map[p.abgabeExtern].push({ type: 'frist_extern', name: p.name, akte: p.akte, id: p.id });
      }
      if (p.abgabeIntern && map[p.abgabeIntern]) {
        map[p.abgabeIntern].push({ type: 'frist_intern', name: p.name, akte: p.akte, id: p.id });
      }
    }
    return map;
  }, [projekte, tage]);

  const kwLabel = useMemo(() => {
    const d = new Date(montag);
    d.setDate(d.getDate() + 3);
    const jan1 = new Date(d.getFullYear(), 0, 1);
    const kw = Math.ceil(((d - jan1) / 86400000 + jan1.getDay() + 1) / 7);
    return `KW ${kw}`;
  }, [montag]);

  // ── Monatsansicht ──
  const monthBase = useMemo(() => {
    const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(1);
    d.setMonth(d.getMonth() + monthOffset);
    return d;
  }, [monthOffset]);

  const monthLabel = useMemo(() =>
    monthBase.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }), [monthBase]);

  const monthDays = useMemo(() => {
    const startDow = (monthBase.getDay() + 6) % 7; // Mo=0
    const start = new Date(monthBase); start.setDate(monthBase.getDate() - startDow);
    const lastDay = new Date(monthBase.getFullYear(), monthBase.getMonth() + 1, 0).getDate();
    const cells = Math.ceil((startDow + lastDay) / 7) * 7;
    return Array.from({ length: cells }, (_, i) => { const d = new Date(start); d.setDate(start.getDate() + i); return d; });
  }, [monthBase]);

  const monthEvents = useMemo(() => {
    const map = {};
    const add = (date, ev) => { if (!date) return; (map[date] = map[date] || []).push(ev); };
    for (const p of (projekte || [])) {
      add(p.ortsterminDatum, { type: 'ortstermin', name: p.name, id: p.id });
      add(p.abgabeExtern, { type: 'frist_extern', name: p.name, id: p.id });
      add(p.abgabeIntern, { type: 'frist_intern', name: p.name, id: p.id });
    }
    return map;
  }, [projekte]);

  const eventColor = (type) => {
    if (type === 'ortstermin') return { bg: 'var(--success-bg)', border: 'var(--success)', text: 'var(--success)', label: 'Ortstermin' };
    if (type === 'frist_extern') return { bg: 'var(--danger-bg)', border: 'var(--danger)', text: 'var(--danger)', label: 'Abgabe extern' };
    return { bg: 'var(--warning-bg)', border: 'var(--warning)', text: 'var(--warning)', label: 'Abgabe intern' };
  };

  return (
    <div className="card" style={{ marginBottom: 'var(--space-5)' }}>
      <div className="card-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
        <span className="card-title" style={{ fontVariantNumeric: 'tabular-nums' }}>{calView === 'woche' ? kwLabel : monthLabel}</span>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <div className="seg" role="tablist" aria-label="Kalender-Ansicht">
            <button role="tab" aria-selected={calView === 'woche'} className={calView === 'woche' ? 'is-active' : ''} onClick={() => setCalView('woche')}>Woche</button>
            <button role="tab" aria-selected={calView === 'monat'} className={calView === 'monat' ? 'is-active' : ''} onClick={() => setCalView('monat')}>Monat</button>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
            <button className="btn btn-ghost btn-sm" onClick={() => calView === 'woche' ? setWeekOffset(w => w - 1) : setMonthOffset(m => m - 1)} style={{ fontSize: 16, padding: '2px 8px' }}>‹</button>
            <button className="btn btn-ghost btn-sm" onClick={() => { setWeekOffset(0); setMonthOffset(0); }} style={{ fontSize: 11 }}>Heute</button>
            <button className="btn btn-ghost btn-sm" onClick={() => calView === 'woche' ? setWeekOffset(w => w + 1) : setMonthOffset(m => m + 1)} style={{ fontSize: 16, padding: '2px 8px' }}>›</button>
          </div>
        </div>
      </div>
      {calView === 'woche' ? (
        <div style={{ overflowX: 'auto' }}>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', minHeight: 120, minWidth: 560 }}>
            {tage.map((tag, i) => {
              const key = tag.toISOString().split('T')[0];
              const isToday = key === today;
              const isWeekend = i >= 5;
              const dayEvents = events[key] || [];
              return (
                <div key={key} style={{
                  borderRight: i < 6 ? '1px solid var(--border-light)' : 'none',
                  padding: '8px 6px',
                  background: isToday ? 'var(--surface-blue)' : isWeekend ? 'var(--surface-light)' : 'transparent',
                  minHeight: 100,
                }}>
                  <div style={{
                    fontSize: 11, fontWeight: 600, textAlign: 'center', marginBottom: 6,
                    color: isToday ? 'var(--vl-blue)' : 'var(--text-tertiary)',
                  }}>
                    <div>{TAGE[i]}</div>
                    <div style={{
                      fontSize: 16, fontWeight: isToday ? 700 : 500,
                      color: isToday ? 'var(--vl-blue)' : 'var(--text-primary)',
                    }}>{tag.getDate()}</div>
                  </div>
                  {dayEvents.map((ev, j) => {
                    const c = eventColor(ev.type);
                    return (
                      <div key={j}
                        onClick={() => onOpen(ev.id)}
                        style={{
                          padding: '4px 7px', marginBottom: 4,
                          background: c.bg, borderLeft: `3px solid ${c.border}`, borderRadius: 'var(--radius-sm)',
                          fontSize: 11, lineHeight: 1.3, cursor: 'pointer',
                        }}
                      >
                        <div style={{ fontWeight: 600, color: c.text, fontSize: 10, marginBottom: 1 }}>{c.label}</div>
                        <div style={{ color: 'var(--text-primary)', fontWeight: 500 }}>{ev.name.split(',')[0]}</div>
                        {ev.standort && <div style={{ color: 'var(--text-tertiary)', fontSize: 10 }}>{ev.standort}</div>}
                      </div>
                    );
                  })}
                </div>
              );
            })}
          </div>
        </div>
      ) : (
        <div style={{ overflowX: 'auto' }}>
          <div style={{ minWidth: 600 }}>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
              {TAGE.map((t) => (
                <div key={t} style={{ textAlign: 'center', fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', padding: '6px 0', borderBottom: '1px solid var(--border-light)' }}>{t}</div>
              ))}
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
              {monthDays.map((d, i) => {
                const key = d.toISOString().split('T')[0];
                const inMonth = d.getMonth() === monthBase.getMonth();
                const isToday = key === today;
                const isWeekend = (i % 7) >= 5;
                const evs = monthEvents[key] || [];
                return (
                  <div key={key} style={{
                    minHeight: 92, padding: 5,
                    borderRight: (i % 7) < 6 ? '1px solid var(--border-light)' : 'none',
                    borderBottom: '1px solid var(--border-light)',
                    background: isToday ? 'var(--surface-blue)' : !inMonth ? 'var(--surface-light)' : isWeekend ? 'rgba(0,0,0,0.015)' : 'transparent',
                    opacity: inMonth ? 1 : 0.5,
                  }}>
                    <div style={{ fontSize: 12, fontWeight: isToday ? 700 : 500, color: isToday ? 'var(--vl-blue)' : 'var(--text-secondary)', textAlign: 'right', marginBottom: 3, fontVariantNumeric: 'tabular-nums' }}>{d.getDate()}</div>
                    {evs.slice(0, 3).map((ev, j) => {
                      const c = eventColor(ev.type);
                      return (
                        <div key={j} onClick={(e) => { e.stopPropagation(); onOpen(ev.id); }}
                          title={`${c.label}: ${ev.name || ''}`}
                          style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '1px 4px', marginBottom: 2, borderRadius: 'var(--radius-sm)', background: c.bg, cursor: 'pointer' }}>
                          <span style={{ width: 5, height: 5, borderRadius: '50%', background: c.border, flexShrink: 0 }} />
                          <span style={{ fontSize: 10, color: 'var(--text-primary)', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{(ev.name || '').split(',')[0]}</span>
                        </div>
                      );
                    })}
                    {evs.length > 3 && <div style={{ fontSize: 9, color: 'var(--text-tertiary)', paddingLeft: 4 }}>+{evs.length - 3} mehr</div>}
                  </div>
                );
              })}
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

const ProjektKpiTile = ({ label, value, sub, color, bg, active, onClick }) => (
  <div
    onClick={onClick}
    style={{
      background: active ? (bg || 'var(--surface-light)') : 'var(--surface)',
      borderRadius: 'var(--radius-md)',
      border: `1.5px solid ${active ? color : 'var(--border-light)'}`,
      padding: 'var(--space-4) var(--space-5)',
      cursor: onClick ? 'pointer' : 'default',
      transition: 'all var(--dur-base) var(--ease-out)',
    }}
  >
    <div className="kpi-value" style={{
      fontSize: 28, fontWeight: 800, color, letterSpacing: '-0.02em', lineHeight: 1,
    }}>
      {value}
    </div>
    <div style={{ fontSize: 12, fontWeight: 600, marginTop: 6, color: 'var(--text-primary)' }}>
      {label}
    </div>
    {sub && (
      <div style={{
        fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2,
        overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
      }}>
        {sub}
      </div>
    )}
  </div>
);

// ──────────────────────────────────────────────────────────────────
// NaechsteFristen — ruhige Kurzliste der nächsten Fristen (Countdown-Pillen)
// Standard-Ansicht im „Fristen"-Tab; der Zeitstrahl ist die Detailstufe.
// ──────────────────────────────────────────────────────────────────
const NaechsteFristen = ({ projekte, onOpen }) => {
  const items = useMemo(() => (projekte || [])
    .filter(p => p.effektiveFrist && p.fristRestDays != null && p.status !== 'archived')
    .sort((a, b) => a.fristRestDays - b.fristRestDays)
    .slice(0, 8), [projekte]);

  if (items.length === 0) {
    return (
      <div className="card" style={{ padding: 'var(--space-8) var(--space-6)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>
        Keine Aufträge mit Frist. Fristen unter „Auftrag" setzen.
      </div>
    );
  }

  return (
    <div className="card reveal" style={{ padding: 0, overflow: 'hidden' }}>
      {items.map((p, i) => {
        const f = fristFarbe(p.fristRestDays);
        const info = getFristInfo(p.fristRestDays, p.effektiveFrist);
        return (
          <button
            key={p.id}
            onClick={() => onOpen(p.id)}
            onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--hover)'; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
            style={{
              display: 'flex', alignItems: 'center', gap: 14, width: '100%', textAlign: 'left',
              padding: '13px 18px', background: 'transparent', border: 'none',
              borderTop: i > 0 ? '1px solid var(--border-light)' : 'none', cursor: 'pointer',
              transition: 'background var(--dur-fast) var(--ease-out)',
            }}
          >
            <span className="pill" style={{ color: f.fg, background: f.bg, fontWeight: 600, whiteSpace: 'nowrap' }}>
              {info ? info.label : '—'}
            </span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontWeight: 600, color: 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                {(p.name || '').split(',')[0] || p.name || 'Ohne Namen'}
              </div>
              {p.standort && (
                <div style={{ fontSize: 12, color: 'var(--text-tertiary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.standort}</div>
              )}
            </div>
            <div style={{ fontSize: 13, color: 'var(--text-secondary)', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>
              {formatDate(p.effektiveFrist)}
            </div>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
              <polyline points="9 18 15 12 9 6" />
            </svg>
          </button>
        );
      })}
    </div>
  );
};

// ──────────────────────────────────────────────────────────────────
// FristenTimeline — horizontaler Zeitstrahl, skaliert auf 300+ Projekte
// Kompakte Balken, Row-Packing, Zoom-Stufen, Direct-Click
// ──────────────────────────────────────────────────────────────────
const ZOOM_LEVELS = [
  { label: 'Kompakt', dayW: 8 },
  { label: 'Normal',  dayW: 18 },
  { label: 'Weit',    dayW: 32 },
];

const FristenTimeline = ({ projekte, onOpen }) => {
  const [hovered, setHovered] = useState(null);
  const [zoom, setZoom] = useState(1);
  const scrolledOnce = useRef(false);
  const containerRef = useRef(null);

  const DAY_W = ZOOM_LEVELS[zoom].dayW;
  const BAR_H = 24;
  const BAR_GAP = 3;
  const BAR_MIN_W = zoom === 0 ? 60 : 100;

  const today = useMemo(() => { const d = new Date(); d.setHours(0,0,0,0); return d; }, []);

  const rangeStart = useMemo(() => {
    let minDays = 14;
    for (const p of projekte) {
      if (p.effektiveFrist) {
        const dd = new Date(p.effektiveFrist); dd.setHours(0,0,0,0);
        const daysBefore = Math.ceil((today - dd) / 86400000);
        if (daysBefore > minDays) minDays = Math.min(daysBefore + 7, 180);
      }
    }
    const d = new Date(today); d.setDate(d.getDate() - minDays); return d;
  }, [today, projekte]);
  const rangeEnd = useMemo(() => { const d = new Date(today); d.setDate(d.getDate() + 90); return d; }, [today]);
  const totalDays = Math.ceil((rangeEnd - rangeStart) / 86400000);
  const totalW = totalDays * DAY_W;
  const todayX = Math.ceil((today - rangeStart) / 86400000) * DAY_W;

  const weeks = useMemo(() => {
    const ws = [];
    const d = new Date(rangeStart);
    d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
    while (d < rangeEnd) {
      ws.push({ offset: Math.ceil((d - rangeStart) / 86400000), label: d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short' }) });
      d.setDate(d.getDate() + 7);
    }
    return ws;
  }, [rangeStart, rangeEnd]);

  const months = useMemo(() => {
    const ms = [];
    const d = new Date(rangeStart);
    d.setDate(1);
    while (d < rangeEnd) {
      const off = Math.ceil((d - rangeStart) / 86400000);
      if (off >= 0) ms.push({ offset: off, label: d.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }) });
      d.setMonth(d.getMonth() + 1);
    }
    return ms;
  }, [rangeStart, rangeEnd]);

  const urgencyColor = (days) => {
    const f = fristFarbe(days);
    return { bg: f.bg, border: f.border, text: f.fg };
  };

  const bars = useMemo(() => {
    const items = projekte
      .filter(p => p.effektiveFrist)
      .map(p => {
        const dd = new Date(p.effektiveFrist); dd.setHours(0,0,0,0);
        const dayOff = Math.ceil((dd - rangeStart) / 86400000);
        return { ...p, dayOff, x: dayOff * DAY_W };
      })
      .filter(m => m.dayOff >= -7 && m.dayOff <= totalDays + 7)
      .sort((a, b) => a.dayOff - b.dayOff);

    const rowEnds = [];
    return items.map(m => {
      const w = BAR_MIN_W;
      let row = 0;
      for (let r = 0; r < rowEnds.length; r++) {
        if (m.x - rowEnds[r] >= 6) { row = r; break; }
        row = r + 1;
      }
      rowEnds[row] = m.x + w;
      return { ...m, row, barW: w };
    });
  }, [projekte, rangeStart, totalDays, DAY_W, BAR_MIN_W]);

  const maxRow = bars.reduce((m, r) => Math.max(m, r.row), -1);
  const HEADER_H = 36;
  const chartH = HEADER_H + (maxRow + 1) * (BAR_H + BAR_GAP) + 24;

  useEffect(() => {
    if (containerRef.current && !scrolledOnce.current && bars.length > 0) {
      scrolledOnce.current = true;
      // Scroll zum ersten (evtl. überfälligen) Balken oder heute, je nachdem was weiter links ist
      const firstBarX = bars[0]?.x ?? todayX;
      const scrollTarget = Math.min(firstBarX, todayX);
      containerRef.current.scrollLeft = Math.max(0, scrollTarget - 40);
    }
  }, [bars.length, todayX]);

  useEffect(() => { scrolledOnce.current = false; }, [zoom]);

  const overdueCount = bars.filter(b => b.fristRestDays < 0).length;

  return (
    <div style={{
      background: 'var(--surface)', borderRadius: 'var(--radius-md)',
      border: '1px solid var(--border-light)', marginBottom: 'var(--space-5)',
    }}>
      <div style={{
        padding: '10px 16px 8px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        borderBottom: '1px solid var(--border-light)',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <span style={{ fontSize: 13, fontWeight: 700 }}>Fristen</span>
          <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
            {bars.length} {bars.length !== 1 ? 'Aufträge' : 'Auftrag'}
            {overdueCount > 0 && <span style={{ color: 'var(--danger)', fontWeight: 600 }}>{' '}· {overdueCount} überfällig</span>}
          </span>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
          {ZOOM_LEVELS.map((z, i) => (
            <button key={i} onClick={() => setZoom(i)} style={{
              fontSize: 10, fontWeight: zoom === i ? 700 : 400,
              padding: '2px 8px', borderRadius: 99, border: 'none', cursor: 'pointer',
              background: zoom === i ? 'var(--vl-blue)' : 'transparent',
              color: zoom === i ? 'white' : 'var(--text-tertiary)',
            }}>{z.label}</button>
          ))}
        </div>
      </div>

      {bars.length === 0 ? (
        <div style={{ padding: '24px 16px', textAlign: 'center', fontSize: 13, color: 'var(--text-tertiary)' }}>
          Keine Aufträge mit Frist. Fristen unter „Auftrag" setzen.
        </div>
      ) : (
        <div ref={containerRef} style={{
          overflowX: 'auto', overflowY: 'visible', cursor: 'grab',
          scrollbarWidth: 'thin', scrollbarColor: 'var(--border-medium, #bbb) transparent',
          paddingBottom: 8,
        }}
          onMouseDown={(e) => {
            const el = containerRef.current;
            if (!el) return;
            const startX = e.pageX, startScroll = el.scrollLeft;
            let dragged = false;
            const onMove = (ev) => {
              if (!dragged && Math.abs(ev.pageX - startX) > 4) { dragged = true; el.style.cursor = 'grabbing'; el.style.userSelect = 'none'; }
              if (dragged) el.scrollLeft = startScroll - (ev.pageX - startX);
            };
            const onUp = () => { el.style.cursor = 'grab'; el.style.userSelect = ''; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
            window.addEventListener('mousemove', onMove);
            window.addEventListener('mouseup', onUp);
          }}
        >
          <div style={{ width: totalW, height: Math.max(chartH, 120), position: 'relative' }}>
            {(zoom === 0 ? months : weeks).map((w, i) => (
              <div key={i} style={{ position: 'absolute', left: w.offset * DAY_W, top: 0, bottom: 0, borderLeft: '1px solid var(--border-light)', opacity: 0.4 }}>
                <div style={{ position: 'absolute', top: 4, left: 6, fontSize: 10, color: 'var(--text-tertiary)', whiteSpace: 'nowrap', fontWeight: 500 }}>{w.label}</div>
              </div>
            ))}
            <div style={{ position: 'absolute', left: todayX, top: 0, bottom: 0, width: 0, borderLeft: '2px dashed var(--vl-orange)', opacity: 0.7, zIndex: 5 }}>
              <div style={{ position: 'absolute', top: 2, left: -16, fontSize: 9, fontWeight: 700, color: 'white', background: 'var(--vl-orange)', borderRadius: 3, padding: '1px 5px', whiteSpace: 'nowrap' }}>Heute</div>
            </div>
            {bars.map(b => {
              const y = HEADER_H + b.row * (BAR_H + BAR_GAP);
              const c = urgencyColor(b.fristRestDays);
              const isHov = hovered === b.id;
              const daysLabel = b.fristRestDays < 0 ? `${Math.abs(b.fristRestDays)}d über` : b.fristRestDays === 0 ? 'Heute' : `${b.fristRestDays}d`;
              return (
                <div key={b.id} onClick={() => onOpen(b.id)} onMouseEnter={() => setHovered(b.id)} onMouseLeave={() => setHovered(null)}
                  style={{
                    position: 'absolute', left: b.x, top: y, width: b.barW, height: BAR_H,
                    background: c.bg, border: `1px solid ${c.border}`, borderRadius: 4,
                    cursor: 'pointer', zIndex: isHov ? 20 : 2,
                    display: 'flex', alignItems: 'center', gap: 4, padding: '0 6px', overflow: 'hidden',
                    transition: 'box-shadow 0.15s, transform 0.15s',
                    boxShadow: isHov ? '0 2px 8px rgba(0,0,0,0.15)' : 'none',
                    transform: isHov ? 'translateY(-1px)' : 'none',
                  }}>
                  <span style={{ fontSize: 11, fontWeight: 600, color: c.text, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>
                    {b.name.split(',')[0]}
                  </span>
                  <span style={{ fontSize: 10, fontWeight: 700, color: c.text, flexShrink: 0, opacity: 0.8 }}>{daysLabel}</span>
                </div>
              );
            })}
            {hovered && (() => {
              const b = bars.find(x => x.id === hovered);
              if (!b) return null;
              const y = HEADER_H + b.row * (BAR_H + BAR_GAP);
              const c = urgencyColor(b.fristRestDays);
              return (
                <div style={{
                  position: 'absolute', left: Math.min(b.x, totalW - 200), top: y + BAR_H + 4,
                  width: 190, padding: '8px 10px',
                  background: 'var(--surface)', border: '1px solid var(--border-light)',
                  borderRadius: 8, boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
                  zIndex: 30, pointerEvents: 'none',
                }}>
                  <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 2 }}>{b.name}</div>
                  {b.akte && <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 2 }}>{b.akte}</div>}
                  {b.auftraggeber && <div style={{ fontSize: 11, color: 'var(--text-secondary)', marginBottom: 2 }}>{b.auftraggeber}</div>}
                  {b.standort && <div style={{ fontSize: 11, color: 'var(--text-secondary)', marginBottom: 4 }}>{b.standort}</div>}
                  <div style={{ fontSize: 11, fontWeight: 700, color: c.text }}>
                    Frist: {b.effektiveFrist ? new Date(b.effektiveFrist).toLocaleDateString('de-DE') : '—'}
                  </div>
                </div>
              );
            })()}
          </div>
        </div>
      )}
    </div>
  );
};

// ──────────────────────────────────────────────────────────────────
// ProjektRow (Card im Listen-Layout)
// ──────────────────────────────────────────────────────────────────
// ── Sortierbare Projekttabelle mit aufklappbaren Zeilen ──

const PROJEKT_COLUMNS = [
  { id: 'createdAt',           label: 'Angelegt',           width: 90,   getValue: p => p.createdAt ? formatDate(p.createdAt.split('T')[0]) : '—', sortValue: p => p.createdAt || '', style: () => ({ fontSize: 11, color: 'var(--text-tertiary)' }) },
  { id: 'name',                label: 'AZ (intern)',         width: 100,  getValue: p => p.name || '—', style: p => ({ fontWeight: 600, fontFamily: 'var(--font-mono, monospace)', fontSize: 12 }) },
  { id: 'adresse',            label: 'Adresse',            width: null, getValue: p => p.adresse || p.standort || '—' },
  { id: 'objektart',          label: 'Objektart',          width: 90,   getValue: p => p.objektart || '—' },
  { id: 'auftraggeber',       label: 'Auftraggeber',       width: 130,  getValue: p => p.auftraggeber || '—' },
  { id: 'sachverstaendiger',  label: 'SV',                 width: 100,  getValue: p => p.sachverstaendiger || '—', style: p => ({ fontSize: 12 }) },
  { id: 'aktenzeichen',       label: 'AZ / GZ',            width: 100,  getValue: p => p.aktenzeichen || p.geschaeftszeichen || '—' },
  { id: 'auftragseingang',    label: 'Auftrag von',        width: 90,   getValue: p => p.auftragseingang ? formatDate(p.auftragseingang) : '—', sortValue: p => p.auftragseingang || '' },
  { id: 'abgabeExtern',       label: 'Auftrag bis',        width: 90,   getValue: p => p.abgabeExtern ? formatDate(p.abgabeExtern) : '—', sortValue: p => p.abgabeExtern || '' },
  { id: 'phase',              label: 'Phase',              width: 90,   getValue: p => null /* custom render */, sortValue: p => getPhase(p).rank },
  { id: 'unterlagen',         label: 'Unterlagen',         width: 80,   getValue: p => `${p.dokDone}/${p.dokTotal}`, sortValue: p => p.dokDone },
];

// Phase/Status eines Auftrags → Pill-Label + Farben. Manueller Status hat Vorrang,
// sonst automatische Ableitung. Modul-Ebene, damit Liste UND Karten ihn nutzen können.
const getPhase = (p) => {
  const manual = p.bearbeitungsstatus && AUFTRAG_STATUS_BY_VALUE[p.bearbeitungsstatus];
  if (manual) return { label: manual.short, color: manual.fg, bg: manual.bg, rank: manual.ord ?? 0 };
  const isArchived = p.status === 'archived';
  if (isArchived || (p.status || '').includes('abgeschlossen')) return { label: 'Abgeschlossen', color: 'var(--success)', bg: 'var(--success-bg)', rank: 5 };
  if (p.ortsterminDatum && new Date(p.ortsterminDatum) < new Date()) return { label: 'Entwurf', color: 'var(--vl-blue)', bg: 'var(--surface-blue)', rank: 3 };
  if (p.ortsterminDatum) return { label: 'Ortstermin', color: 'var(--vl-orange-dark)', bg: 'var(--vl-orange-bg)', rank: 2 };
  if (p.gutachtenCount > 0 && p.standort) return { label: 'Unterlagen', color: 'var(--text-secondary)', bg: 'var(--surface-light)', rank: 1 };
  return { label: 'Anlage', color: 'var(--text-tertiary)', bg: 'var(--surface-light)', rank: 0 };
};

// Gemeinsamer Titelbild-Loader (Signed-URL). Antwortfeld 'url' (mit signedUrl-Fallback).
const ProjektImage = ({ path, session, workerUrl, style, iconSize = 18 }) => {
  const [url, setUrl] = useState(null);
  useEffect(() => {
    let active = true;
    if (!path || !session?.access_token || !workerUrl) { setUrl(null); return; }
    fetch(`${workerUrl}/api/signed-url?path=${encodeURIComponent(path)}`, { headers: { Authorization: `Bearer ${session.access_token}` } })
      .then(r => (r.ok ? r.json() : null))
      .then(d => { const u = d && (d.url || d.signedUrl); if (active && u) setUrl(u); })
      .catch(() => {});
    return () => { active = false; };
  }, [path, session, workerUrl]);
  if (url) return <img src={url} alt="" style={{ ...style, objectFit: 'cover' }} />;
  return (
    <span style={{ ...style, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-tertiary)', background: 'var(--surface-light)' }}>
      <svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /><polyline points="9 22 9 12 15 12 15 22" /></svg>
    </span>
  );
};

const ProjektThumb = ({ path, session, workerUrl }) => (
  <ProjektImage path={path} session={session} workerUrl={workerUrl} iconSize={22}
    style={{ width: 56, height: 56, borderRadius: 'var(--radius-md)', flexShrink: 0, border: '1px solid var(--border-light)', boxShadow: 'var(--shadow-subtle)' }} />
);

// ── Karten-Ansicht: bildbetonte Kachel je Auftrag ──
const ProjektCard = ({ p, onOpen, session, workerUrl }) => {
  const phase = getPhase(p);
  const fristInfo = getFristInfo(p.fristRestDays, p.effektiveFrist);
  const titel = (p.adresse || '').split(',')[0] || p.name || 'Ohne Adresse';
  const metaTeile = [p.standort, p.objektart].filter(Boolean);
  const pct = p.dokTotal ? p.dokDone / p.dokTotal : 0;
  const barColor = pct >= 1 ? 'var(--success)' : pct > 0 ? 'var(--vl-orange)' : 'var(--text-tertiary)';
  return (
    <button onClick={() => onOpen(p.id)} className="card lift"
      style={{ padding: 0, overflow: 'hidden', cursor: 'pointer', display: 'flex', flexDirection: 'column', textAlign: 'left', border: '1px solid var(--border-light)', background: 'var(--surface)' }}>
      <div style={{ height: 120, position: 'relative', background: 'var(--surface-light)' }}>
        <ProjektImage path={p.titelbildPath} session={session} workerUrl={workerUrl} iconSize={30}
          style={{ width: '100%', height: '100%', display: 'block' }} />
        <span style={{ position: 'absolute', top: 10, left: 10 }}>
          <span className="pill" style={{ color: phase.color, background: phase.bg, whiteSpace: 'nowrap', boxShadow: 'var(--shadow-subtle)' }}>{phase.label}</span>
        </span>
      </div>
      <div style={{ padding: '13px 15px 15px', flex: 1, display: 'flex', flexDirection: 'column', gap: 8 }}>
        <div>
          <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{titel}</div>
          {metaTeile.length > 0 && (
            <div style={{ fontSize: 12.5, color: 'var(--text-tertiary)', marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{metaTeile.join(' · ')}</div>
          )}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 'auto', paddingTop: 6 }}>
          {fristInfo
            ? <span className="pill" style={{ color: fristInfo.color, background: fristInfo.bg, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }} title={fristInfo.label}>{fristInfo.datum ? formatDate(fristInfo.datum) : fristInfo.label}</span>
            : <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Keine Frist</span>}
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, flexShrink: 0 }} title={`${p.dokDone}/${p.dokTotal} Unterlagen`}>
            <span style={{ width: 46, height: 5, borderRadius: 3, background: 'var(--border-light)', overflow: 'hidden' }}>
              <span style={{ display: 'block', height: '100%', width: `${pct * 100}%`, background: barColor, borderRadius: 3 }} />
            </span>
            <span style={{ fontSize: 12, color: 'var(--text-secondary)', fontVariantNumeric: 'tabular-nums' }}>{p.dokDone}/{p.dokTotal}</span>
          </span>
        </div>
      </div>
    </button>
  );
};

const ProjektKarten = ({ projekte, onOpen, session, workerUrl }) => {
  if (!projekte || projekte.length === 0) {
    return (
      <div className="card" style={{ padding: 'var(--space-8) var(--space-6)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>
        Keine Aufträge gefunden.
      </div>
    );
  }
  return (
    <div className="reveal" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 16 }}>
      {projekte.map(p => <ProjektCard key={p.id} p={p} onOpen={onOpen} session={session} workerUrl={workerUrl} />)}
    </div>
  );
};

const ProjektTable = ({ projekte, sort, setSort, onOpen, onArchive, onUnarchive, onDelete, onStatusChange, session, workerUrl }) => {
  const [expandedId, setExpandedId] = useState(null);
  const [statusMenu, setStatusMenu] = useState(null); // { id, top, left, auftragId, current }
  const openStatusMenu = (e, p) => {
    e.stopPropagation();
    if (statusMenu && statusMenu.id === p.id) { setStatusMenu(null); return; }
    const r = e.currentTarget.getBoundingClientRect();
    const estH = 250;
    const below = r.bottom + 6;
    const top = (below + estH > window.innerHeight) ? Math.max(8, r.top - 6 - estH) : below;
    setStatusMenu({ id: p.id, top, left: r.left, auftragId: p.auftrag_id, current: p.bearbeitungsstatus || 'angelegt' });
  };

  // Sortierung
  const sorted = useMemo(() => {
    const col = PROJEKT_COLUMNS.find(c => c.id === sort.col);
    if (!col) return projekte;
    const dir = sort.dir === 'asc' ? 1 : -1;
    return [...projekte].sort((a, b) => {
      const va = col.sortValue ? col.sortValue(a) : (col.getValue(a) || '');
      const vb = col.sortValue ? col.sortValue(b) : (col.getValue(b) || '');
      if (va === vb) return 0;
      if (va === '—' || va === '') return 1;
      if (vb === '—' || vb === '') return -1;
      return va < vb ? -dir : dir;
    });
  }, [projekte, sort]);

  const toggleSort = (colId) => {
    setSort(prev =>
      prev.col === colId
        ? { col: colId, dir: prev.dir === 'asc' ? 'desc' : 'asc' }
        : { col: colId, dir: 'asc' }
    );
  };

  const thStyle = {
    padding: '8px 10px', fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)',
    textTransform: 'uppercase', letterSpacing: '0.05em', whiteSpace: 'nowrap',
    borderBottom: '2px solid var(--border-light)', textAlign: 'left', cursor: 'pointer',
    userSelect: 'none', position: 'sticky', top: 0, background: 'var(--surface)',
  };
  const tdStyle = {
    padding: '10px 10px', fontSize: 13, borderBottom: '1px solid var(--border-light)',
    verticalAlign: 'top', color: 'var(--text-primary)',
  };
  const detailLabel = { fontSize: 11, color: 'var(--text-tertiary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' };
  const sortArrow = (colId) => {
    if (sort.col !== colId) return '';
    return sort.dir === 'asc' ? ' ↑' : ' ↓';
  };

  if (sorted.length === 0) {
    return (
      <div style={{
        padding: 'var(--space-6)', textAlign: 'center',
        color: 'var(--text-tertiary)', fontSize: 14,
        background: 'var(--surface)', borderRadius: 'var(--radius-md)',
        border: '1px dashed var(--border-light)',
      }}>
        Keine Aufträge entsprechen dem Filter.
      </div>
    );
  }

  return (
    <div className="appear" style={{ overflowX: 'auto', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-md)', background: 'var(--surface)' }}>
      <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 1020 }}>
        <thead>
          <tr>
            <th style={{ ...thStyle, width: 64, cursor: 'default' }} />
            {PROJEKT_COLUMNS.map(col => (
              <th key={col.id} onClick={() => toggleSort(col.id)}
                style={{ ...thStyle, width: col.width || undefined }}>
                {col.label}{sortArrow(col.id)}
              </th>
            ))}
            <th style={{ ...thStyle, width: 36, cursor: 'default' }} />
          </tr>
        </thead>
        <tbody>
          {sorted.map(p => {
            const phase = getPhase(p);
            const isExpanded = expandedId === p.id;
            const isArchived = p.status === 'archived';
            const fristInfo = getFristInfo(p.fristRestDays, p.effektiveFrist);

            return (
              <Fragment key={p.id}>
                <tr
                  className="proj-row"
                  onClick={() => setExpandedId(isExpanded ? null : p.id)}
                  style={{
                    cursor: 'pointer', opacity: isArchived ? 0.6 : 1,
                    background: isExpanded ? 'var(--surface-light)' : 'transparent',
                    transition: 'background 0.15s',
                  }}
                  onMouseEnter={e => { if (!isExpanded) e.currentTarget.style.background = 'var(--hover)'; }}
                  onMouseLeave={e => { if (!isExpanded) e.currentTarget.style.background = 'transparent'; }}
                >
                  <td style={{ ...tdStyle, width: 64, padding: '10px 0 10px 12px' }}>
                    <ProjektThumb path={p.titelbildPath} session={session} workerUrl={workerUrl} />
                  </td>
                  {PROJEKT_COLUMNS.map(col => (
                    <td key={col.id} style={tdStyle}>
                      {col.id === 'phase' ? (
                        onStatusChange && p.auftrag_id ? (
                          <button
                            onClick={(e) => openStatusMenu(e, p)}
                            title="Status ändern"
                            className="pill"
                            style={{ color: phase.color, background: phase.bg, whiteSpace: 'nowrap', border: 'none', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 5 }}
                          >
                            {phase.label}
                            <svg width="9" height="6" viewBox="0 0 10 6" fill="none" style={{ opacity: 0.55, flexShrink: 0 }}><path d="M1 1l4 4 4-4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
                          </button>
                        ) : (
                          <span className="pill" style={{ color: phase.color, background: phase.bg, whiteSpace: 'nowrap' }}>{phase.label}</span>
                        )
                      ) : col.id === 'unterlagen' ? (
                        (() => {
                          const done = p.dokDone || 0, total = p.dokTotal || 0;
                          const pct = total > 0 ? done / total : 0;
                          const barCol = pct >= 1 ? 'var(--success)' : pct > 0 ? 'var(--vl-orange)' : 'var(--text-tertiary)';
                          return (
                            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
                              <span style={{ width: 40, height: 5, borderRadius: 3, background: 'var(--border-light)', overflow: 'hidden', flexShrink: 0 }}>
                                <span style={{ display: 'block', height: '100%', width: `${pct * 100}%`, background: barCol, borderRadius: 3, transition: 'width var(--dur-slow) var(--ease-out)' }} />
                              </span>
                              <span style={{ fontSize: 12, fontVariantNumeric: 'tabular-nums', color: 'var(--text-secondary)' }}>{done}/{total}</span>
                            </span>
                          );
                        })()
                      ) : col.id === 'adresse' ? (
                        (() => {
                          const full = p.adresse || p.standort || '';
                          if (!full) return <span style={{ color: 'var(--text-tertiary)' }}>—</span>;
                          const teile = full.split(',');
                          const strasse = teile[0].trim();
                          const ort = teile.slice(1).join(',').trim();
                          return (
                            <div>
                              <div style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)' }}>{strasse}</div>
                              {ort && <div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 1 }}>{ort}</div>}
                            </div>
                          );
                        })()
                      ) : col.id === 'abgabeExtern' && fristInfo ? (
                        <span className="pill" style={{ color: fristInfo.color, background: fristInfo.bg, fontVariantNumeric: 'tabular-nums' }} title={fristInfo.label}>
                          {col.getValue(p)}
                        </span>
                      ) : (
                        <span style={{ fontSize: 12, ...(col.style ? col.style(p) : {}) }}>{col.getValue(p)}</span>
                      )}
                    </td>
                  ))}
                  <td style={{ ...tdStyle, textAlign: 'right', padding: '10px 10px', whiteSpace: 'nowrap' }}>
                    <span className="row-actions" style={{ display: 'inline-flex', alignItems: 'center', gap: 2, marginRight: 6, verticalAlign: 'middle' }}>
                      <button onClick={(e) => { e.stopPropagation(); onOpen(p.id); }} title="Auftrag öffnen"
                        style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 5, borderRadius: 'var(--radius-sm)', color: 'var(--text-secondary)', display: 'inline-flex' }}
                        onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'none'; }}>
                        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
                      </button>
                      {!isArchived ? (
                        <button onClick={(e) => { e.stopPropagation(); onArchive(p); }} title="Archivieren"
                          style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 5, borderRadius: 'var(--radius-sm)', color: 'var(--text-secondary)', display: 'inline-flex' }}
                          onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'none'; }}>
                          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" /><line x1="10" y1="12" x2="14" y2="12" /></svg>
                        </button>
                      ) : (
                        <button onClick={(e) => { e.stopPropagation(); onUnarchive(p); }} title="Wiederherstellen"
                          style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 5, borderRadius: 'var(--radius-sm)', color: 'var(--text-secondary)', display: 'inline-flex' }}
                          onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'none'; }}>
                          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7v6h6" /><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" /></svg>
                        </button>
                      )}
                    </span>
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
                      style={{ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s', verticalAlign: 'middle' }}>
                      <polyline points="6 9 12 15 18 9" />
                    </svg>
                  </td>
                </tr>

                {/* Aufgeklappter Bereich */}
                {isExpanded && (
                  <tr>
                    <td colSpan={PROJEKT_COLUMNS.length + 2} style={{ padding: 0, borderBottom: '2px solid var(--vl-blue)' }}>
                      <div style={{
                        padding: '16px 20px', background: 'var(--surface-light)',
                        display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '12px 24px',
                        fontSize: 13,
                      }}>
                        <div>
                          <span style={detailLabel}>Interne Notiz</span>
                          <div style={{ marginTop: 3, color: p.interneNotiz ? 'var(--text-primary)' : 'var(--text-tertiary)' }}>
                            {p.interneNotiz || '—'}
                          </div>
                        </div>
                        <div>
                          <span style={detailLabel}>Sachverständiger</span>
                          <div style={{ marginTop: 3 }}>{p.sachverstaendiger || '—'}</div>
                        </div>
                        <div>
                          <span style={detailLabel}>Sachbearbeiter</span>
                          <div style={{ marginTop: 3 }}>{p.sachbearbeiter || '—'}</div>
                        </div>
                        <div>
                          <span style={detailLabel}>Auftragsart</span>
                          <div style={{ marginTop: 3 }}>{p.auftragsart || '—'}</div>
                        </div>
                        <div>
                          <span style={detailLabel}>Ortstermin</span>
                          <div style={{ marginTop: 3, fontVariantNumeric: 'tabular-nums' }}>{p.ortsterminDatum ? formatDate(p.ortsterminDatum) : '—'}</div>
                        </div>
                        <div>
                          <span style={detailLabel}>Frist</span>
                          <div style={{ marginTop: 4 }}>
                            {fristInfo
                              ? <span className="pill" style={{ color: fristInfo.color, background: fristInfo.bg }}>{fristInfo.label}</span>
                              : <span style={{ color: 'var(--text-tertiary)' }}>Keine Frist</span>}
                          </div>
                        </div>
                      </div>
                      <div style={{
                        padding: '10px 20px', display: 'flex', gap: 8,
                        borderTop: '1px solid var(--border-light)', background: 'var(--surface-light)',
                      }}>
                        <button className="btn btn-primary btn-sm"
                          onClick={(e) => { e.stopPropagation(); onOpen(p.id); }}
                          style={{ fontSize: 12, minHeight: 32, padding: '6px 16px' }}>
                          Auftrag öffnen
                        </button>
                        {!isArchived ? (
                          <button className="btn btn-ghost btn-sm"
                            onClick={(e) => { e.stopPropagation(); onArchive(p); }}
                            style={{ fontSize: 12, minHeight: 32, padding: '6px 12px' }}>
                            Archivieren
                          </button>
                        ) : (
                          <button className="btn btn-ghost btn-sm"
                            onClick={(e) => { e.stopPropagation(); onUnarchive(p); }}
                            style={{ fontSize: 12, minHeight: 32, padding: '6px 12px' }}>
                            Wiederherstellen
                          </button>
                        )}
                        <button className="btn btn-ghost btn-sm"
                          onClick={(e) => { e.stopPropagation(); onDelete(p); }}
                          style={{ fontSize: 12, minHeight: 32, padding: '6px 12px', color: 'var(--danger)' }}>
                          Löschen
                        </button>
                      </div>
                    </td>
                  </tr>
                )}
              </Fragment>
            );
          })}
        </tbody>
      </table>
      {statusMenu && onStatusChange && ReactDOM.createPortal(
        <>
          <div onClick={(e) => { e.stopPropagation(); setStatusMenu(null); }} style={{ position: 'fixed', inset: 0, zIndex: 9998 }} />
          <div onMouseDown={(e) => e.stopPropagation()}
            style={{ position: 'fixed', top: statusMenu.top, left: statusMenu.left, zIndex: 9999, background: 'var(--surface)', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-md)', boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 200 }}>
            {AUFTRAG_STATUS_OPTIONS.map(opt => {
              const isCurrent = statusMenu.current === opt.value;
              return (
                <button key={opt.value}
                  onClick={(e) => { e.stopPropagation(); const aid = statusMenu.auftragId; setStatusMenu(null); if (!isCurrent && aid) onStatusChange(aid, opt.value); }}
                  style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left', padding: '7px 10px', background: isCurrent ? 'var(--hover)' : 'transparent', border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }}
                  onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)'; }}
                  onMouseLeave={e => { e.currentTarget.style.background = isCurrent ? 'var(--hover)' : 'transparent'; }}>
                  <span className="pill" style={{ color: opt.fg, background: opt.bg, whiteSpace: 'nowrap' }}>{opt.short}</span>
                  {isCurrent && (
                    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--success)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginLeft: 'auto', flexShrink: 0 }}><polyline points="20 6 9 17 4 12" /></svg>
                  )}
                </button>
              );
            })}
          </div>
        </>,
        document.body
      )}
    </div>
  );
};

function menuItemStyle(color) {
  return {
    display: 'block', width: '100%', textAlign: 'left',
    padding: '8px 14px', background: 'none', border: 'none',
    fontSize: 13, color: color || 'var(--text-primary)',
    cursor: 'pointer', transition: 'background 0.1s',
  };
}

// ──────────────────────────────────────────────────────────────────
// ConfirmDialog — generischer Bestätigungsdialog für Delete-Aktionen
// ──────────────────────────────────────────────────────────────────
const ConfirmDialog = ({ title, message, confirmLabel = 'Bestätigen', danger = false, onConfirm, onCancel, busy = false }) => (
  <div
    className="modal-backdrop"
    onClick={(e) => { if (e.target === e.currentTarget && !busy) onCancel(); }}
  >
    <div className="modal" style={{ maxWidth: 460 }}>
      <div className="modal-header">
        <div className="modal-title">{title}</div>
      </div>
      <div className="modal-body" style={{ padding: 'var(--space-5)' }}>
        <div style={{ fontSize: 14, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
          {message}
        </div>
      </div>
      <div className="modal-footer" style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-3)' }}>
        <button className="btn btn-ghost" onClick={onCancel} disabled={busy}>Abbrechen</button>
        <button
          className={danger ? 'btn btn-danger' : 'btn btn-primary'}
          onClick={onConfirm} disabled={busy}
          style={danger ? { background: 'var(--danger)', color: 'white', border: 'none' } : {}}
        >
          {busy ? 'Läuft …' : confirmLabel}
        </button>
      </div>
    </div>
  </div>
);

const ProjektlisteView = ({ onOpen, onNewProjekt, session, workerUrl, userProfile }) => {
  const isMobile = useIsMobile();
  const [projekte, setProjekte] = useState(null);
  const [error, setError] = useState(null);
  const [filter, setFilter] = useState('alle');  // 'alle' | 'meine' | 'ueberfaellig' | 'diese_woche' | 'ohne_frist' | 'archiv'
  const [svFilter, setSvFilter] = useState(null); // null = alle, sonst Name des Sachverständigen
  const [search, setSearch] = useState('');
  const [sort, setSort] = useState({ col: 'createdAt', dir: 'desc' });
  const [viewMode, setViewMode] = useState('liste'); // 'liste' | 'karten' | 'kalender' | 'fristen'
  const [fristenSubView, setFristenSubView] = useState('kompakt'); // 'kompakt' | 'gantt'
  // Bestätigungs-/Aktions-State
  const [confirmState, setConfirmState] = useState(null);  // { type, projekt, busy, error }

  const reload = useCallback(() => {
    ladeProjektliste()
      .then(data => setProjekte(data))
      .catch(err => setError(err.message || String(err)));
  }, []);

  // Bearbeitungsstatus direkt aus der Liste setzen (optimistisch, dann Server-Sync)
  const handleStatusChange = useCallback(async (auftragId, value) => {
    if (!auftragId) return;
    setProjekte(prev => (prev || []).map(p => p.auftrag_id === auftragId ? { ...p, bearbeitungsstatus: value } : p));
    try {
      await apiPatchRow('auftraege', auftragId, { bearbeitungsstatus: value }, session, workerUrl);
    } catch (err) {
      setError(err.message || String(err));
    } finally {
      reload();
    }
  }, [session, workerUrl, reload]);

  useEffect(() => {
    let active = true;
    ladeProjektliste()
      .then(data => { if (active) setProjekte(data); })
      .catch(err => { if (active) setError(err.message || String(err)); });
    return () => { active = false; };
  }, []);

  // Aktive (nicht-archivierte) Projekte für die KPI-Zählungen
  const aktive = useMemo(
    () => (projekte || []).filter(p => p.status !== 'archived'),
    [projekte]
  );
  const archiviert = useMemo(
    () => (projekte || []).filter(p => p.status === 'archived'),
    [projekte]
  );

  // KPI-Gruppen berechnen (nur aktive)
  const overdue = useMemo(
    () => aktive.filter(p => p.fristRestDays != null && p.fristRestDays < 0),
    [aktive]
  );
  const thisWeek = useMemo(
    () => aktive.filter(p => p.fristRestDays != null && p.fristRestDays >= 0 && p.fristRestDays <= 7),
    [aktive]
  );
  const noFrist = useMemo(
    () => aktive.filter(p => p.fristRestDays == null),
    [aktive]
  );
  const meine = useMemo(() => {
    const myName = userProfile?.full_name;
    if (!myName) return [];
    const q = myName.toLowerCase();
    return aktive.filter(p =>
      (p.sachverstaendiger || '').toLowerCase().includes(q) ||
      (p.sachbearbeiter || '').toLowerCase().includes(q)
    );
  }, [aktive, userProfile]);
  const nextDeadline = useMemo(() => {
    return aktive
      .filter(p => p.fristRestDays != null && p.fristRestDays >= 0)
      .sort((a, b) => a.fristRestDays - b.fristRestDays)[0];
  }, [aktive]);

  // Eindeutige Sachverständige aus allen Projekten (für SV-Filter)
  const sachverstaendigeList = useMemo(() => {
    if (!projekte) return [];
    const names = new Set();
    for (const p of projekte) {
      if (p.sachverstaendiger) names.add(p.sachverstaendiger);
    }
    return [...names].sort();
  }, [projekte]);

  // Gefilterte + sortierte Liste
  const gefiltert = useMemo(() => {
    if (!projekte) return [];
    let list;
    if (filter === 'archiv') list = archiviert;
    else if (filter === 'meine') list = meine;
    else if (filter === 'ueberfaellig') list = overdue;
    else if (filter === 'diese_woche') list = thisWeek;
    else if (filter === 'ohne_frist') list = noFrist;
    else list = aktive;

    // Sachverständiger-Filter
    if (svFilter) {
      list = list.filter(p => (p.sachverstaendiger || '') === svFilter);
    }

    if (search.trim()) {
      const q = search.trim().toLowerCase();
      list = list.filter(p =>
        (p.name || '').toLowerCase().includes(q) ||
        (p.akte || '').toLowerCase().includes(q) ||
        (p.aktenzeichen || '').toLowerCase().includes(q) ||
        (p.geschaeftszeichen || '').toLowerCase().includes(q) ||
        (p.standort || '').toLowerCase().includes(q) ||
        (p.adresse || '').toLowerCase().includes(q) ||
        (p.auftraggeber || '').toLowerCase().includes(q) ||
        (p.sachverstaendiger || '').toLowerCase().includes(q) ||
        (p.sachbearbeiter || '').toLowerCase().includes(q)
      );
    }

    return list;
  }, [projekte, filter, svFilter, search, aktive, archiviert, overdue, thisWeek, noFrist, meine]);

  // ── Aktionen ──────────────────────────────────────────────────
  const handleArchive = (projekt) => {
    setConfirmState({ type: 'archive', projekt, busy: false, error: null });
  };
  const handleUnarchive = (projekt) => {
    setConfirmState({ type: 'unarchive', projekt, busy: false, error: null });
  };
  const handleDelete = (projekt) => {
    setConfirmState({ type: 'delete', projekt, busy: false, error: null });
  };

  const executeConfirm = async () => {
    if (!confirmState) return;
    setConfirmState(s => ({ ...s, busy: true, error: null }));
    try {
      if (confirmState.type === 'archive') {
        await apiArchiveProjekt(confirmState.projekt.id, true, session, workerUrl);
      } else if (confirmState.type === 'unarchive') {
        await apiArchiveProjekt(confirmState.projekt.id, false, session, workerUrl);
      } else if (confirmState.type === 'delete') {
        await apiDeleteProjekt(confirmState.projekt.id, session, workerUrl);
      }
      setConfirmState(null);
      reload();
    } catch (err) {
      setConfirmState(s => ({ ...s, busy: false, error: err.message || String(err) }));
    }
  };

  return (
    <div className="view-wrapper">
      <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
        <div>
          <div className="page-title">Aufträge</div>
          <div className="page-subtitle">
            {projekte
              ? (filter === 'archiv'
                  ? `${archiviert.length} archiviert`
                  : `${aktive.length} ${aktive.length === 1 ? 'aktiver Auftrag' : 'aktive Aufträge'}` +
                    (archiviert.length > 0 ? ` · ${archiviert.length} archiviert` : ''))
              : 'Lade Aufträge…'}
          </div>
        </div>
        <button
          className="btn btn-primary"
          onClick={onNewProjekt}
          style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
        >
          + Neuer Auftrag
        </button>
      </div>

      {error && (
        <PrototypeHint>
          <strong>Fehler beim Laden:</strong> {error}
        </PrototypeHint>
      )}

      {!projekte && !error && (
        <div style={{ padding: 'var(--space-8)', textAlign: 'center', color: 'var(--text-tertiary)' }}>
          Lade…
        </div>
      )}

      {projekte && projekte.length === 0 && (
        <div className="card" style={{ padding: 'var(--space-8) var(--space-6)', maxWidth: 640, margin: '0 auto' }}>
          <div style={{ textAlign: 'center', marginBottom: 'var(--space-6)' }}>
            <div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 8 }}>
              Willkommen bei Augenschein
            </div>
            <div style={{ color: 'var(--text-secondary)', fontSize: 14, lineHeight: 1.5 }}>
              Vom Gerichtsbeschluss zum fertigen Gutachten in drei Schritten.
            </div>
          </div>

          <div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 'var(--space-4)', marginBottom: 'var(--space-6)' }}>
            {[
              { step: '1', title: 'Auftrag anlegen', desc: 'Beschluss und Anschreiben als PDF hochladen. Stammdaten, Beteiligte und Fristen werden automatisch ausgelesen.' },
              { step: '2', title: 'Ortstermin einsprechen', desc: 'Beim Ortstermin die Begehung per Sprachaufnahme diktieren. Fotos direkt zuordnen.' },
              { step: '3', title: 'Entwurf generieren', desc: 'Aus Stammdaten und Ortstermin-Notizen wird ein Gutachten-Entwurf erzeugt. Inline bearbeiten und exportieren.' },
            ].map(s => (
              <div key={s.step} style={{
                flex: 1, padding: 'var(--space-4)',
                background: 'var(--surface-light)', borderRadius: 'var(--radius-md)',
                border: '1px solid var(--border-light)',
              }}>
                <div style={{
                  width: 28, height: 28, borderRadius: '50%',
                  background: 'var(--vl-blue)', color: 'white',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  fontSize: 13, fontWeight: 700, marginBottom: 10,
                }}>{s.step}</div>
                <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 6 }}>
                  {s.title}
                </div>
                <div style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
                  {s.desc}
                </div>
              </div>
            ))}
          </div>

          <div style={{ textAlign: 'center' }}>
            <button className="btn btn-primary" onClick={onNewProjekt}
              style={{ padding: '14px 32px', fontSize: 15 }}>
              + Ersten Auftrag anlegen
            </button>
          </div>
        </div>
      )}

      {projekte && projekte.length > 0 && (
        <>
          {/* KPI-Kacheln — kompakt */}
          <div style={{
            display: 'flex', gap: 'var(--space-3)', flexWrap: 'wrap',
            marginBottom: 'var(--space-4)',
          }}>
            <ProjektKpiTile
              label="Aktiv"
              value={aktive.length}
              color="var(--vl-blue)"
              bg="var(--surface-blue)"
              active={filter === 'alle'}
              onClick={() => setFilter('alle')}
            />
            {overdue.length > 0 && (
              <ProjektKpiTile
                label="Überfällig"
                value={overdue.length}
                color="var(--danger)"
                bg="var(--danger-bg)"
                active={filter === 'ueberfaellig'}
                onClick={() => setFilter(filter === 'ueberfaellig' ? 'alle' : 'ueberfaellig')}
              />
            )}
            {thisWeek.length > 0 && (
              <ProjektKpiTile
                label="Diese Woche"
                value={thisWeek.length}
                color="var(--warning)"
                bg="var(--warning-bg)"
                active={filter === 'diese_woche'}
                onClick={() => setFilter(filter === 'diese_woche' ? 'alle' : 'diese_woche')}
              />
            )}
            {archiviert.length > 0 && (
              <ProjektKpiTile
                label="Archiv"
                value={archiviert.length}
                color="var(--text-tertiary)"
                bg="var(--surface-light)"
                active={filter === 'archiv'}
                onClick={() => setFilter(filter === 'archiv' ? 'alle' : 'archiv')}
              />
            )}
          </div>

          {/* Ansichts-Umschalter: Liste · Kalender · Fristen — Apple Segmented Control */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 'var(--space-4)' }}>
            <div className="seg" role="tablist" aria-label="Ansicht">
              <button role="tab" aria-selected={viewMode === 'liste'} className={viewMode === 'liste' ? 'is-active' : ''}
                onClick={() => setViewMode('liste')}>Liste</button>
              <button role="tab" aria-selected={viewMode === 'karten'} className={viewMode === 'karten' ? 'is-active' : ''}
                onClick={() => setViewMode('karten')}>Karten</button>
              <button role="tab" aria-selected={viewMode === 'kalender'} className={viewMode === 'kalender' ? 'is-active' : ''}
                onClick={() => setViewMode('kalender')}>Kalender</button>
              <button role="tab" aria-selected={viewMode === 'fristen'} className={viewMode === 'fristen' ? 'is-active' : ''}
                onClick={() => setViewMode('fristen')}>Fristen</button>
            </div>
          </div>

          {viewMode === 'kalender' ? (
            <WochenKalender projekte={aktive} onOpen={onOpen} />
          ) : viewMode === 'fristen' ? (
            <div className="appear">
              <div style={{ display: 'flex', marginBottom: 'var(--space-3)' }}>
                <div className="seg" role="tablist" aria-label="Fristen-Ansicht">
                  <button role="tab" aria-selected={fristenSubView === 'kompakt'} className={fristenSubView === 'kompakt' ? 'is-active' : ''}
                    onClick={() => setFristenSubView('kompakt')}>Nächste Fristen</button>
                  <button role="tab" aria-selected={fristenSubView === 'gantt'} className={fristenSubView === 'gantt' ? 'is-active' : ''}
                    onClick={() => setFristenSubView('gantt')}>Zeitstrahl</button>
                </div>
              </div>
              {fristenSubView === 'gantt'
                ? <FristenTimeline projekte={projekte} onOpen={onOpen} />
                : <NaechsteFristen projekte={projekte} onOpen={onOpen} />}
            </div>
          ) : (
          <>

          {/* Search + SV-Filter + Sort */}
          <div style={{
            display: 'flex', alignItems: 'center', gap: 'var(--space-3)',
            marginBottom: 'var(--space-3)', flexWrap: 'wrap',
          }}>
            <div style={{ position: 'relative', flex: 1, minWidth: 200, maxWidth: 360 }}>
              <svg
                width="14" height="14" viewBox="0 0 24 24" fill="none"
                stroke="var(--text-tertiary)" strokeWidth="2"
                style={{
                  position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)',
                }}
              >
                <circle cx="11" cy="11" r="8"/>
                <line x1="21" y1="21" x2="16.65" y2="16.65"/>
              </svg>
              <input
                type="text"
                placeholder="Name, AZ Gericht, Adresse, Auftraggeber…"
                value={search}
                onChange={e => setSearch(e.target.value)}
                style={{
                  width: '100%',
                  padding: '10px 14px 10px 36px',
                  borderRadius: 'var(--radius-md)',
                  border: '1px solid var(--border-medium)',
                  background: 'var(--surface)',
                  fontSize: 13, outline: 'none',
                }}
              />
              {search && (
                <button
                  onClick={() => setSearch('')}
                  style={{
                    position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
                    background: 'none', border: 'none', color: 'var(--text-tertiary)',
                    cursor: 'pointer', fontSize: 16, padding: 2,
                  }}
                >
                  ×
                </button>
              )}
            </div>

            {/* Sachverständiger-Filter */}
            {sachverstaendigeList.length > 1 && (
              <select
                value={svFilter || ''}
                onChange={e => setSvFilter(e.target.value || null)}
                style={{
                  padding: '9px 28px 9px 12px',
                  borderRadius: 'var(--radius-md)',
                  border: `1.5px solid ${svFilter ? 'var(--vl-blue, #0A2540)' : 'var(--border-light)'}`,
                  background: svFilter ? 'var(--vl-blue-dim, rgba(10,37,64,0.08))' : 'var(--surface)',
                  fontSize: 13, outline: 'none', cursor: 'pointer',
                  color: svFilter ? 'var(--vl-blue, #0A2540)' : 'var(--text-secondary)',
                  fontWeight: svFilter ? 600 : 400,
                  appearance: 'none',
                  backgroundImage: `url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23666' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")`,
                  backgroundRepeat: 'no-repeat',
                  backgroundPosition: 'right 10px center',
                }}
              >
                <option value="">Sachverständiger</option>
                {sachverstaendigeList.map(sv => (
                  <option key={sv} value={sv}>{sv}</option>
                ))}
              </select>
            )}
            <div style={{ display: 'flex', gap: 4 }}>
              {[
                { col: 'phase', dir: 'asc', label: 'Phase' },
                { col: 'abgabeExtern', dir: 'asc', label: 'Frist' },
                { col: 'name', dir: 'asc', label: 'AZ' },
                { col: 'createdAt', dir: 'desc', label: 'Neueste' },
              ].map(s => (
                <button
                  key={s.col}
                  onClick={() => setSort({ col: s.col, dir: s.dir })}
                  style={{
                    padding: '7px 12px', borderRadius: 'var(--radius-sm)',
                    fontSize: 12, fontWeight: 600, cursor: 'pointer',
                    border: `1px solid ${sort.col === s.col ? 'var(--vl-blue, #0A2540)' : 'var(--border-light)'}`,
                    background: sort.col === s.col ? 'var(--vl-blue-dim, rgba(10,37,64,0.08))' : 'transparent',
                    color: sort.col === s.col ? 'var(--vl-blue, #0A2540)' : 'var(--text-secondary)',
                    transition: 'all 0.15s',
                  }}
                >
                  {s.label}
                </button>
              ))}
            </div>
          </div>

          {/* Filter-Hinweis falls aktiv */}
          {(filter !== 'alle' || svFilter) && (
            <div style={{
              padding: 'var(--space-2) var(--space-3)',
              background: 'var(--surface)',
              border: '1px solid var(--border-light)',
              borderRadius: 'var(--radius-sm)',
              marginBottom: 'var(--space-3)',
              fontSize: 12, color: 'var(--text-secondary)',
              display: 'flex', alignItems: 'center', gap: 'var(--space-2)', flexWrap: 'wrap',
            }}>
              <span>
                Gefiltert:
                {filter !== 'alle' && (
                  <> {filter === 'ueberfaellig' ? 'Überfällig'
                    : filter === 'diese_woche' ? 'Diese Woche'
                    : filter === 'archiv' ? 'Archiviert'
                    : filter === 'meine' ? 'Meine'
                    : 'Ohne Frist'}</>
                )}
                {filter !== 'alle' && svFilter && ' + '}
                {svFilter && <> SV: {svFilter}</>}
                {' '}({gefiltert.length} Ergebnis{gefiltert.length !== 1 ? 'se' : ''})
              </span>
              {svFilter && (
                <button
                  onClick={() => setSvFilter(null)}
                  style={{
                    background: 'none', border: 'none',
                    color: 'var(--vl-blue-light)', cursor: 'pointer', fontSize: 12,
                  }}
                >
                  SV-Filter löschen
                </button>
              )}
              {filter !== 'alle' && (
                <button
                  onClick={() => setFilter('alle')}
                  style={{
                    background: 'none', border: 'none',
                    color: 'var(--vl-blue-light)', cursor: 'pointer', fontSize: 12,
                  }}
                >
                  {svFilter ? 'Status-Filter löschen' : 'Filter löschen'}
                </button>
              )}
              {(filter !== 'alle' && svFilter) && (
                <button
                  onClick={() => { setFilter('alle'); setSvFilter(null); }}
                  style={{
                    marginLeft: 'auto', background: 'none', border: 'none',
                    color: 'var(--danger)', cursor: 'pointer', fontSize: 12, fontWeight: 600,
                  }}
                >
                  Alle Filter löschen
                </button>
              )}
            </div>
          )}

          {/* Projektliste / Kartenraster — geteilte Filterzeile darüber */}
          <div className="project-list">
            {viewMode === 'karten' ? (
              <ProjektKarten
                projekte={gefiltert}
                onOpen={onOpen}
                session={session}
                workerUrl={workerUrl}
              />
            ) : (
              <ProjektTable
                projekte={gefiltert}
                sort={sort}
                setSort={setSort}
                onOpen={onOpen}
                onArchive={handleArchive}
                onUnarchive={handleUnarchive}
                onDelete={handleDelete}
                onStatusChange={handleStatusChange}
                session={session}
                workerUrl={workerUrl}
              />
            )}
          </div>
          </>
          )}
        </>
      )}

      {confirmState && (
        <ConfirmDialog
          title={
            confirmState.type === 'archive' ? 'Auftrag archivieren'
            : confirmState.type === 'unarchive' ? 'Auftrag wiederherstellen'
            : 'Auftrag endgültig löschen'
          }
          message={
            confirmState.type === 'archive'
              ? <>
                  Auftrag <strong>{confirmState.projekt.name}</strong> wird archiviert und aus der aktiven Liste entfernt.
                  Die Daten bleiben erhalten und können über den Filter „Archiviert" wiederhergestellt werden.
                  {confirmState.error && <div style={{ color: 'var(--danger)', marginTop: 8 }}>{confirmState.error}</div>}
                </>
              : confirmState.type === 'unarchive'
              ? <>
                  Auftrag <strong>{confirmState.projekt.name}</strong> wird in die aktive Liste zurückverschoben.
                  {confirmState.error && <div style={{ color: 'var(--danger)', marginTop: 8 }}>{confirmState.error}</div>}
                </>
              : <>
                  Auftrag <strong>{confirmState.projekt.name}</strong> wird <strong>unwiderruflich</strong> gelöscht, inklusive aller Gutachten, Bewertungsobjekte, Beteiligten, Dokumente und Ortstermin-Notizen.
                  <div style={{
                    marginTop: 'var(--space-3)', padding: 'var(--space-3)',
                    background: 'var(--danger-bg, rgba(220, 38, 38, 0.05))',
                    borderLeft: '3px solid var(--danger)',
                    fontSize: 12,
                  }}>
                    Diese Aktion kann nicht rückgängig gemacht werden. Erwäge stattdessen die Archivierung.
                  </div>
                  {confirmState.error && <div style={{ color: 'var(--danger)', marginTop: 8 }}>{confirmState.error}</div>}
                </>
          }
          confirmLabel={
            confirmState.type === 'archive' ? 'Archivieren'
            : confirmState.type === 'unarchive' ? 'Wiederherstellen'
            : 'Endgültig löschen'
          }
          danger={confirmState.type === 'delete'}
          busy={confirmState.busy}
          onConfirm={executeConfirm}
          onCancel={() => setConfirmState(null)}
        />
      )}
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════
// 4.2 · VIEW: DASHBOARD
// ══════════════════════════════════════════════════════════════════

// ──────────────────────────────────────────────────────────────────
// Projekt-Lifecycle-Stepper — zeigt den aktuellen Bearbeitungsstand
// ──────────────────────────────────────────────────────────────────
const LIFECYCLE_STEPS = [
  { id: 'eingang',     label: 'Eingang' },
  { id: 'unterlagen',  label: 'Unterlagen' },
  { id: 'ortstermin',  label: 'Ortstermin' },
  { id: 'entwurf',     label: 'Entwurf' },
  { id: 'abgabe',      label: 'Abgabe' },
];

function getLifecycleStatus(p) {
  const hasDokumente = (p.dokumente || []).length > 0;
  const g0 = (p.gutachten || [])[0];
  const ortsterminDone = g0?.ortsterminStatus === 'abgeschlossen';
  const ortsterminGeplant = g0?.ortsterminStatus === 'geplant' || g0?.ortsterminStatus === 'terminiert';
  const entwurfPct = g0?.entwurfPct || 0;
  const abgeschlossen = (p.status || '').toLowerCase().includes('abgeschlossen');

  return {
    eingang:    'done',
    unterlagen: hasDokumente ? 'done' : 'active',
    ortstermin: ortsterminDone ? 'done' : (hasDokumente && ortsterminGeplant) ? 'active' : 'pending',
    entwurf:    entwurfPct >= 100 ? 'done' : (ortsterminDone && entwurfPct > 0) ? 'active' : (ortsterminDone ? 'active' : 'pending'),
    abgabe:     abgeschlossen ? 'done' : 'pending',
  };
}

const ProjektStickyHeader = ({ p, view, gutachtenTab, navigate, setGutachtenTab, orgMembers, session, workerUrl, onRefresh }) => {
  const isMobile = useIsMobile();
  const tabBarRef = useRef(null);
  const activeTabRef = useRef(null);
  const [hoveredTab, setHoveredTab] = useState(null);
  const fristDanger = p.fristRestDays != null && p.fristRestDays < 0;
  const fristWarn = p.fristRestDays != null && p.fristRestDays <= 14 && !fristDanger;

  const agoLine = useMemo(() => {
    if (!p.updatedAt) return null;
    const member = (orgMembers || []).find(m => m.id === p.updatedBy);
    const ago = Math.round((Date.now() - new Date(p.updatedAt).getTime()) / 60000);
    const agoText = ago < 1 ? 'gerade eben' : ago < 60 ? `vor ${ago} Min.` : ago < 1440 ? `vor ${Math.round(ago / 60)} Std.` : `vor ${Math.round(ago / 1440)} Tagen`;
    return member ? `${member.name}, ${agoText}` : agoText;
  }, [p.updatedAt, p.updatedBy, orgMembers]);

  const TABS = useMemo(() => [
    { id: 'dashboard', label: isMobile ? '⊞' : '⊞ Dashboard' },
    { id: 'auftrag', label: 'Auftrag' },
    { id: 'unterlagen', label: isMobile ? 'Docs' : 'Unterlagen' },
    { id: 'stammdaten', label: isMobile ? 'Obj.' : 'Objekte' },
    { id: 'ortstermin', label: isMobile ? 'OT' : 'Ortstermin' },
    { id: 'fotos', label: 'Fotos' },
    { id: 'entwurf', label: 'Entwurf' },
    { id: 'gutachten', label: isMobile ? 'GA' : 'Gutachten' },
  ], [isMobile]);

  // Aktiven Tab horizontal in den sichtbaren Bereich rücken — wichtig mobil,
  // wenn man über eine Aktion direkt zu einem hinteren Tab springt. Scrollt
  // nur die Tab-Leiste selbst, nicht die Seite.
  useEffect(() => {
    const bar = tabBarRef.current, btn = activeTabRef.current;
    if (!bar || !btn) return;
    const target = btn.offsetLeft - (bar.clientWidth - btn.clientWidth) / 2;
    bar.scrollTo({ left: Math.max(0, target), behavior: 'smooth' });
  }, [view, gutachtenTab]);

  return (
    <div style={{
      position: 'sticky', top: isMobile ? 44 : 56, zIndex: 20,
      background: 'var(--glass-bg)',
      WebkitBackdropFilter: 'saturate(180%) blur(20px)',
      backdropFilter: 'saturate(180%) blur(20px)',
      borderBottom: '1px solid var(--glass-border)',
      boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.5)',
      marginBottom: 'var(--space-4)',
    }}>
      {/* Projekt-Identität */}
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start',
        padding: isMobile ? '8px 4px 0' : '12px 4px 0', gap: 8,
      }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
            {p.akte && (
              <span style={{
                fontSize: isMobile ? 10 : 12, fontWeight: 600, color: 'var(--vl-blue)',
                background: 'var(--surface-blue, #EEF3F8)', padding: '2px 8px',
                borderRadius: 4, whiteSpace: 'nowrap',
              }}>{p.akte}</span>
            )}
            <EditableField
              value={p.name}
              placeholder="AZ (intern)"
              onSave={async (v) => {
                await apiPatchRow('projects', p.id, { name: v }, session, workerUrl);
                onRefresh && onRefresh();
              }}
              style={{
                fontSize: isMobile ? 16 : 20, fontWeight: 800, letterSpacing: '-0.01em',
                color: 'var(--text-primary)', lineHeight: 1.2,
              }}
            />
          </div>
          {!isMobile && (
            <div style={{
              display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap',
              fontSize: 12, color: 'var(--text-secondary)', marginTop: 2, paddingBottom: 2,
            }}>
              {p.auftragsart && <span>{p.auftragsart}</span>}
              {p.auftragstyp && <React.Fragment><span style={{ color: 'var(--border-medium)' }}>·</span><span>{p.auftragstyp}</span></React.Fragment>}
              {p.verfahren && <React.Fragment><span style={{ color: 'var(--border-medium)' }}>·</span><span>{p.verfahren}</span></React.Fragment>}
              <span style={{ color: 'var(--border-medium)' }}>·</span>
              <Pill variant="active" style={{ fontSize: 10 }}>{p.status || 'offen'}</Pill>
              {agoLine && <React.Fragment><span style={{ color: 'var(--border-medium)' }}>·</span><span style={{ color: 'var(--text-tertiary)', fontSize: 11 }}>{agoLine}</span></React.Fragment>}
            </div>
          )}
        </div>
        {/* Deadline badge */}
        {p.fristExtern && (
          <div style={{
            padding: isMobile ? '4px 8px' : '6px 12px', borderRadius: 'var(--radius-sm)', flexShrink: 0, textAlign: 'right',
            background: fristDanger ? 'var(--danger-bg)' : fristWarn ? 'var(--warning-bg)' : 'var(--surface-light)',
            border: `1px solid ${fristDanger ? 'var(--danger)' : fristWarn ? '#F2D5A8' : 'var(--border-light)'}`,
          }}>
            {!isMobile && <div style={{ fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: fristDanger ? 'var(--danger)' : fristWarn ? 'var(--warning)' : 'var(--text-tertiary)' }}>Abgabe</div>}
            <div style={{ fontSize: isMobile ? 12 : 13, fontWeight: 700, color: 'var(--text-primary)' }}>{p.fristExtern}</div>
            {p.fristRestDays != null && (
              <div style={{ fontSize: 11, fontWeight: 600, color: fristDanger ? 'var(--danger)' : fristWarn ? 'var(--warning)' : 'var(--text-tertiary)' }}>
                {p.fristRestDays < 0 ? `${p.fristRestDays} T.` : `${p.fristRestDays} T.`}
              </div>
            )}
          </div>
        )}
      </div>
      {/* Tabs */}
      <div ref={tabBarRef} style={{
        display: 'flex', gap: 0, overflowX: 'auto', WebkitOverflowScrolling: 'touch',
        paddingLeft: 4, scrollbarWidth: 'none',
      }}>
        {TABS.map(tab => {
          const isActive = tab.id === 'dashboard' ? view === 'dashboard'
            : tab.id === 'auftrag' ? view === 'auftrag'
            : view === 'gutachten' && gutachtenTab === tab.id;
          return (
            <button key={tab.id}
              ref={isActive ? activeTabRef : null}
              onMouseEnter={() => setHoveredTab(tab.id)}
              onMouseLeave={() => setHoveredTab(null)}
              onClick={() => {
              if (tab.id === 'dashboard') navigate('dashboard');
              else if (tab.id === 'auftrag') navigate('auftrag');
              else { navigate('gutachten'); setGutachtenTab(tab.id); }
            }}
              style={{
                padding: isMobile ? '8px 10px' : '10px 16px', fontSize: isMobile ? 12 : 13, fontWeight: isActive ? 700 : 400,
                color: isActive ? 'var(--vl-blue)' : (hoveredTab === tab.id ? 'var(--vl-blue-light)' : 'var(--text-tertiary)'),
                background: 'none', border: 'none', cursor: 'pointer',
                borderBottom: isActive ? '2px solid var(--vl-orange)' : '2px solid transparent',
                marginBottom: -2, whiteSpace: 'nowrap', flexShrink: 0,
                transition: 'color 0.15s',
              }}
            >
              {tab.label}
            </button>
          );
        })}
      </div>
    </div>
  );
};

const AuftragKachel = ({ p, onOpen }) => {
  const az = p.aktenzeichen || p.geschaeftszeichen;
  return (
    <div className="kachel">
      <div className="kachel-header">
        <span className="kachel-title">Auftrag</span>
        <span className="kachel-count">{p.auftragsart === 'Gerichtsauftrag' ? 'Gericht' : p.auftragsart}</span>
      </div>
      <div className="kachel-body" style={{ fontSize: 13 }}>
        {p.akte && <div className="kachel-entry"><span className="kachel-entry-label">Auftragsnummer</span><span className="kachel-entry-value" style={{ fontWeight: 600 }}>{p.akte}</span></div>}
        <div className="kachel-entry"><span className="kachel-entry-label">Auftraggeber</span><span className="kachel-entry-value">{p.auftraggeber || '—'}</span></div>
        {p.verfahren && <div className="kachel-entry"><span className="kachel-entry-label">Verfahren</span><span className="kachel-entry-value">{p.verfahren}</span></div>}
        {az && <div className="kachel-entry"><span className="kachel-entry-label">AZ / GZ</span><span className="kachel-entry-value">{az}</span></div>}
        {p.auftragseingang && <div className="kachel-entry"><span className="kachel-entry-label">Auftrag von</span><span className="kachel-entry-value">{p.auftragseingang}</span></div>}
        {(p.fristExtern || p.fristIntern) && (
          <div className="kachel-entry">
            <span className="kachel-entry-label">Auftrag bis</span>
            <span className="kachel-entry-value" style={{
              color: p.fristRestDays != null && p.fristRestDays < 0 ? 'var(--danger)' : p.fristRestDays != null && p.fristRestDays <= 7 ? '#EAB308' : undefined,
              fontWeight: p.fristRestDays != null && p.fristRestDays <= 7 ? 700 : undefined,
            }}>
              {p.fristExtern || '—'}{p.fristIntern ? ` (intern: ${p.fristIntern})` : ''}
            </span>
          </div>
        )}
        {p.sachverstaendiger && <div className="kachel-entry"><span className="kachel-entry-label">Sachverständiger</span><span className="kachel-entry-value">{p.sachverstaendiger}</span></div>}
        {p.sachbearbeiter && <div className="kachel-entry"><span className="kachel-entry-label">Sachbearbeiter</span><span className="kachel-entry-value">{p.sachbearbeiter}</span></div>}
        <div className="kachel-entry"><span className="kachel-entry-label">Ortstermin</span><span className="kachel-entry-value">{p.ortstermin}</span></div>
        {p.ausfertigungen != null && <div className="kachel-entry"><span className="kachel-entry-label">Ausfertigungen</span><span className="kachel-entry-value">{p.ausfertigungen}× Druck</span></div>}
        {p.notizen && (
          <div className="kachel-entry" style={{ flexDirection: 'column', gap: 2 }}>
            <span className="kachel-entry-label">Notizen</span>
            <span className="kachel-entry-value" style={{ fontSize: 12, color: 'var(--text-secondary)', whiteSpace: 'pre-wrap' }}>{p.notizen}</span>
          </div>
        )}
      </div>
      <div className="kachel-footer">
        <button className="kachel-footer-link" onClick={onOpen}>
          Details öffnen <IconChevronRight size={16} />
        </button>
      </div>
    </div>
  );
};

const BeteiligteKachel = ({ p, onOpen }) => (
  <div className="kachel">
    <div className="kachel-header">
      <span className="kachel-title">Beteiligte</span>
      <span className="kachel-count">{p.beteiligte.length} Rollen</span>
    </div>
    <div className="kachel-body" style={{ padding: 'var(--space-3)' }}>
      {p.beteiligte.map((b, i) => (
        <div key={i} style={{
          display: 'flex', alignItems: 'baseline', gap: 8,
          padding: '4px 0', fontSize: 13,
          borderBottom: i < p.beteiligte.length - 1 ? '1px solid var(--border-light)' : 'none',
        }}>
          <span style={{
            fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em',
            color: 'var(--text-tertiary)', flexShrink: 0, minWidth: 90,
          }}>{b.rolle}</span>
          <span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}>
            <span style={{ fontWeight: 500, color: 'var(--text-primary)' }}>{b.name}</span>
            {(b.telefon || b.email) && (
              <span style={{ fontSize: 11, color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {[b.telefon, b.email].filter(Boolean).join(' · ')}
              </span>
            )}
          </span>
        </div>
      ))}
      {p.beteiligte.length === 0 && (
        <div style={{ fontSize: 13, color: 'var(--text-tertiary)', padding: '8px 0' }}>Keine Beteiligten erfasst</div>
      )}
    </div>
    <div className="kachel-footer">
      <button className="kachel-footer-link" onClick={onOpen}>
        Alle bearbeiten <IconChevronRight size={16} />
      </button>
    </div>
  </div>
);

const GutachtenKachel = ({ gutachten, onOpen }) => {
  const g = gutachten;
  return (
    <div className="kachel kachel-gutachten">
      <div className="kachel-header">
        <span className="kachel-title">Gutachten</span>
        <span className="kachel-count">
          {g.objekte.length} Objekt{g.objekte.length === 1 ? '' : 'e'}
        </span>
      </div>
      <div className="kachel-body">
        <div style={{ fontWeight: 600, color: 'var(--text-primary)', fontSize: 15 }}>{g.adresse}</div>
        {g.objekte.length > 1 && (
          <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 4 }}>
            {g.objekte.map(o => o.bezeichnung).join(' · ')}
          </div>
        )}
        <div style={{ marginTop: 8 }}>
          <ProgressRow label="Unterlagen" value={g.unterlagenDone} total={g.unterlagenTotal} />
          <ProgressRow label="Ortstermin" value={g.ortsterminStatus === 'begangen' ? 1 : 0} total={1} variant="accent" />
          <ProgressRow label="Entwurf" value={g.entwurfPct} total={100} variant={g.entwurfPct >= 80 ? 'success' : ''} />
        </div>
      </div>
      <div className="kachel-footer">
        <button className="kachel-footer-link" onClick={onOpen}>
          Gutachten öffnen <IconChevronRight size={16} />
        </button>
      </div>
    </div>
  );
};

const GutachtenCard = ({ gutachten, idx, onOpen, onDelete, projectId, session, workerUrl, onRefresh }) => {
  const g = gutachten;
  const [imgUrl, setImgUrl] = useState(null);
  const [uploading, setUploading] = useState(false);
  const [confirmDel, setConfirmDel] = useState(false);
  const fileRef = useRef(null);

  // Signed URL für Titelbild laden
  useEffect(() => {
    if (!g.titelbild_path || !session?.access_token || !workerUrl) return;
    let cancelled = false;
    fetch(`${workerUrl}/api/signed-url?path=${encodeURIComponent(g.titelbild_path)}`, {
      headers: { Authorization: `Bearer ${session.access_token}` },
    })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (!cancelled && d?.url) setImgUrl(d.url); })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [g.titelbild_path, session, workerUrl]);

  const handleUpload = async (e) => {
    e.stopPropagation();
    const file = e.target?.files?.[0];
    if (!file || !session?.access_token) return;
    setUploading(true);
    try {
      // Bild komprimieren (max 800px Breite)
      const blob = await new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
          const maxW = 800;
          const scale = img.width > maxW ? maxW / img.width : 1;
          const c = document.createElement('canvas');
          c.width = img.width * scale;
          c.height = img.height * scale;
          c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
          c.toBlob(b => resolve(b), 'image/jpeg', 0.85);
        };
        img.src = URL.createObjectURL(file);
      });

      const fd = new FormData();
      fd.append('file', blob, 'titelbild.jpg');
      fd.append('project_id', projectId);
      fd.append('gutachten_id', g.id);
      const upRes = await fetch(`${workerUrl}/api/photo-upload`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${session.access_token}` },
        body: fd,
      });
      if (!upRes.ok) throw new Error('Upload fehlgeschlagen');
      const { storage_path } = await upRes.json();

      // Pfad auf Gutachten speichern
      await apiPatchRow('gutachten', g.id, { titelbild_path: storage_path }, session, workerUrl);
      if (onRefresh) onRefresh();
    } catch (err) {
      console.error('Titelbild-Upload:', err);
    } finally {
      setUploading(false);
      if (fileRef.current) fileRef.current.value = '';
    }
  };

  const imgSize = 80;

  return (
    <div className="gutachten-card" onClick={() => onOpen(idx)} style={{ display: 'flex', gap: 'var(--space-3)', position: 'relative' }}
      onMouseEnter={e => { const btn = e.currentTarget.querySelector('.gc-delete-btn'); if (btn) btn.style.opacity = '1'; }}
      onMouseLeave={e => { const btn = e.currentTarget.querySelector('.gc-delete-btn'); if (btn) btn.style.opacity = '0'; }}
    >
      {/* Titelbild / Upload-Zone */}
      <div
        onClick={(e) => { e.stopPropagation(); fileRef.current?.click(); }}
        onMouseEnter={(e) => { const ov = e.currentTarget.querySelector('.img-overlay'); if (ov) ov.style.opacity = '1'; }}
        onMouseLeave={(e) => { const ov = e.currentTarget.querySelector('.img-overlay'); if (ov) ov.style.opacity = '0'; }}
        style={{
          width: imgSize, height: imgSize, flexShrink: 0,
          borderRadius: 'var(--radius-sm)', overflow: 'hidden',
          background: 'var(--surface-light)',
          border: imgUrl ? '1px solid var(--border-light)' : '1px dashed var(--border-light)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          cursor: 'pointer', position: 'relative',
        }}
        title={g.titelbild_path ? 'Klicken zum Ändern' : 'Titelbild hinzufügen'}
      >
        {imgUrl ? (
          <>
            <img src={imgUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
            <div className="img-overlay" style={{
              position: 'absolute', inset: 0,
              background: 'rgba(0,0,0,0.4)',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              opacity: 0, transition: 'opacity 0.2s',
              color: 'white', fontSize: 11, fontWeight: 600,
            }}>Ändern</div>
          </>
        ) : (
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="1.5">
            <rect x="3" y="3" width="18" height="18" rx="2"/>
            <circle cx="8.5" cy="8.5" r="1.5"/>
            <polyline points="21 15 16 10 5 21"/>
          </svg>
        )}
        {uploading && (
          <div style={{
            position: 'absolute', inset: 0,
            background: 'rgba(255,255,255,0.7)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 10, fontWeight: 600, color: 'var(--text-secondary)',
          }}>...</div>
        )}
        <input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }}
          onChange={handleUpload} />
      </div>

      {/* Content */}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div className="gutachten-card-num">Gutachten {idx + 1}</div>
        <div className="gutachten-card-title">{g.titel}</div>
        <div className="gutachten-card-address">{g.adresse}</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 8 }}>
          {/* Mini Progress Ring */}
          {(() => {
            const total = (g.unterlagenTotal || 1);
            const pctUnterlagen = g.unterlagenTotal > 0 ? g.unterlagenDone / total : 0;
            const pctEntwurf = (g.entwurfPct || 0) / 100;
            const combined = (pctUnterlagen * 0.5 + pctEntwurf * 0.5);
            const r = 14, circ = 2 * Math.PI * r;
            const dashOff = circ - combined * circ;
            const color = combined >= 0.8 ? 'var(--success)' : combined >= 0.3 ? '#EAB308' : 'var(--text-tertiary)';
            return (
              <svg width="36" height="36" viewBox="0 0 36 36" style={{ flexShrink: 0 }}>
                <circle cx="18" cy="18" r={r} fill="none" stroke="var(--border-light)" strokeWidth="3" />
                <circle cx="18" cy="18" r={r} fill="none" stroke={color} strokeWidth="3"
                  strokeDasharray={circ} strokeDashoffset={dashOff}
                  strokeLinecap="round" transform="rotate(-90 18 18)"
                  style={{ transition: 'stroke-dashoffset 0.4s ease' }} />
                <text x="18" y="18" textAnchor="middle" dominantBaseline="central"
                  fontSize="10" fontWeight="600" fill="var(--text-secondary)">
                  {Math.round(combined * 100)}%
                </text>
              </svg>
            );
          })()}
          <div className="gutachten-card-stats" style={{ margin: 0 }}>
            <span><strong>{g.objekte.length}</strong> Objekt{g.objekte.length === 1 ? '' : 'e'}</span>
            <span>·</span>
            <span>Unterlagen <strong>{g.unterlagenDone}/{g.unterlagenTotal}</strong></span>
            <span>·</span>
            <span>Entwurf <strong>{g.entwurfPct}%</strong></span>
          </div>
        </div>
      </div>

      {/* Delete button */}
      {onDelete && (
        <button
          onClick={(e) => { e.stopPropagation(); setConfirmDel(true); }}
          title="Gutachten löschen"
          style={{
            position: 'absolute', top: 8, right: 8,
            width: 28, height: 28, borderRadius: '50%',
            background: 'none', border: 'none', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            opacity: 0, transition: 'opacity 0.15s',
            fontSize: 16, color: 'var(--text-tertiary)',
          }}
          className="gc-delete-btn"
        >×</button>
      )}

      {/* Confirm */}
      {confirmDel && (
        <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
          onClick={(e) => { e.stopPropagation(); setConfirmDel(false); }}>
          <div className="card" style={{ maxWidth: 380, padding: 24 }} onClick={e => e.stopPropagation()}>
            <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 8, color: 'var(--danger)' }}>Gutachten löschen</div>
            <p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16, lineHeight: 1.5 }}>
              <strong>{g.titel || `Gutachten ${idx + 1}`}</strong> wird unwiderruflich gelöscht.
            </p>
            <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
              <button className="btn btn-ghost" onClick={(e) => { e.stopPropagation(); setConfirmDel(false); }}>Abbrechen</button>
              <button className="btn" style={{ background: 'var(--danger)', color: '#fff' }}
                onClick={async (e) => { e.stopPropagation(); setConfirmDel(false); if (onDelete) await onDelete(g.id); }}
              >Löschen</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

const DokumentePanel = ({ dokumente, onUpload, onOpenDocument, onDropFile, onDeleteDocument }) => {
  const [dragOver, setDragOver] = useState(false);
  const scopeLabel = (scope) => scope === 'auftrag' ? 'Auftrag' : scope === 'gutachten' ? 'Gutachten' : 'Objekt';
  const statusLabel = (status) => status === 'done' ? '✓ vorhanden' : 'fehlt';

  return (
    <div className="card"
      onDragOver={e => { e.preventDefault(); e.stopPropagation(); setDragOver(true); }}
      onDragEnter={e => { e.preventDefault(); e.stopPropagation(); setDragOver(true); }}
      onDragLeave={e => { e.preventDefault(); e.stopPropagation(); setDragOver(false); }}
      onDrop={e => {
        e.preventDefault(); e.stopPropagation(); setDragOver(false);
        const f = e.dataTransfer?.files?.[0];
        if (f && onDropFile) onDropFile(f);
        else if (f) onUpload();
      }}
      style={dragOver ? { outline: '2px dashed var(--vl-blue)', outlineOffset: -2, background: 'var(--surface-blue, #EEF3F8)' } : {}}
    >
      <div className="card-header">
        <span className="card-title">Dokumente ({dokumente.length})</span>
        <button className="btn btn-accent btn-sm" onClick={onUpload}>+ Hinzufügen</button>
      </div>
      <div className="doc-list">
        {dokumente.map((d, i) => {
          const clickable = !!d.id && !!onOpenDocument;
          return (
            <div
              key={d.id || i}
              className="doc-row"
              onClick={clickable ? () => onOpenDocument(d.id) : undefined}
              style={clickable ? { cursor: 'pointer' } : undefined}
              title={clickable ? 'Klicken zum Öffnen' : undefined}
            >
              <div className="doc-icon"><IconDocument size={20} /></div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div className="doc-name">{d.label}</div>
                <div className="doc-meta">{d.typ}</div>
              </div>
              <span className={`doc-scope doc-scope-${d.scope}`}>{scopeLabel(d.scope)}</span>
              <span className={`doc-status doc-status-${d.status}`}>{statusLabel(d.status)}</span>
              {onDeleteDocument && (
                <button
                  type="button"
                  title="Dokument löschen"
                  onClick={(e) => { e.stopPropagation(); onDeleteDocument(d.id, d.label); }}
                  style={{
                    background: 'none', border: 'none', cursor: 'pointer',
                    padding: 4, borderRadius: 'var(--radius-sm)',
                    color: 'var(--text-tertiary)', display: 'flex', alignItems: 'center',
                  }}
                  onMouseEnter={e => { e.currentTarget.style.color = 'var(--danger)'; }}
                  onMouseLeave={e => { e.currentTarget.style.color = 'var(--text-tertiary)'; }}
                >
                  <IconClose size={14} />
                </button>
              )}
              <IconChevronRight size={16} stroke="var(--text-tertiary)" />
            </div>
          );
        })}
      </div>
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// DashboardHero — „Auf einen Blick": Fortschritts-Stepper (Status-Stufen,
// antippbar) + Kennzahlen (Frist, Unterlagen, offene Aufgaben, Ortstermin).
// Reine Darstellung/Navigation; das Statussetzen nutzt dieselbe Operation
// wie das bestehende Status-Dropdown (apiPatchRow auftraege.bearbeitungsstatus).
// ────────────────────────────────────────────────────────────────────
const DashboardHero = ({ p, session, workerUrl, onRefresh }) => {
  const [localStatus, setLocalStatus] = useState(null);
  const [openTasks, setOpenTasks] = useState(null);
  const [nextDue, setNextDue] = useState(null);

  useEffect(() => {
    let active = true;
    if (!p.id || !session?.access_token) return;
    (async () => {
      let token = session.access_token;
      try { const sb = await initSupabase(); const { data } = await sb.auth.getSession(); if (data?.session?.access_token) token = data.session.access_token; } catch {}
      try {
        const res = await fetch(`${workerUrl}/api/aktivitaeten?project_id=${encodeURIComponent(p.id)}`, { headers: { Authorization: `Bearer ${token}` } });
        if (!res.ok) return;
        const data = await res.json();
        const offen = (data.aktivitaeten || []).filter(a => a.typ === 'aufgabe' && !a.erledigt);
        if (!active) return;
        setOpenTasks(offen.length);
        setNextDue(offen.map(a => a.faellig_am).filter(Boolean).sort()[0] || null);
      } catch {}
    })();
    return () => { active = false; };
  }, [p.id, session, workerUrl]);

  const currentVal = localStatus || p.bearbeitungsstatus || 'angelegt';
  const currentOrd = AUFTRAG_STATUS_BY_VALUE[currentVal]?.ord ?? 0;
  const setStatus = async (value) => {
    if (!p.auftrag_id || value === currentVal) return;
    setLocalStatus(value);
    try { await apiPatchRow('auftraege', p.auftrag_id, { bearbeitungsstatus: value }, session, workerUrl); if (onRefresh) onRefresh(); }
    catch { setLocalStatus(null); }
  };

  const fristInfo = getFristInfo(p.fristRestDays, p.fristExtern);
  const dokDone = (p.gutachten || []).reduce((s, g) => s + (g.unterlagenDone || 0), 0);
  const dokTotal = (p.gutachten || []).reduce((s, g) => s + (g.unterlagenTotal || 0), 0);
  const dokPct = dokTotal ? Math.round(dokDone / dokTotal * 100) : 0;
  const ortRaw = (p.ortstermin && p.ortstermin !== '—') ? p.ortstermin : null;
  const ortDate = ortRaw ? (ortRaw.match(/\d{2}\.\d{2}\.\d{4}/)?.[0] || ortRaw) : null;

  return (
    <div className="card dash-hero">
      <div className="dash-stepper" role="list" aria-label="Auftragsstatus">
        {AUFTRAG_STATUS_OPTIONS.map(st => {
          const done = st.ord < currentOrd;
          const cur = st.ord === currentOrd;
          return (
            <button key={st.value} role="listitem"
              className={`dash-step${cur ? ' is-current' : ''}${done ? ' is-done' : ''}`}
              onClick={() => setStatus(st.value)} title={`Status setzen: ${st.label}`}>
              <span className="dash-step-dot" style={cur ? { background: st.fg, borderColor: st.fg, color: '#fff' } : undefined}>
                {done ? '✓' : st.ord + 1}
              </span>
              <span className="dash-step-label">{st.short.replace(/^\d+\s/, '')}</span>
            </button>
          );
        })}
      </div>

      <div className="dash-stats">
        <div className="dash-stat" style={fristInfo ? { background: fristInfo.bg, borderColor: 'transparent' } : undefined}>
          <div className="dash-stat-label">Frist</div>
          <div className="dash-stat-value" style={fristInfo ? { color: fristInfo.color } : undefined}>{fristInfo ? fristInfo.label : 'Keine Frist'}</div>
          <div className="dash-stat-sub">{fristInfo ? fristInfo.datum : 'kein Abgabetermin'}</div>
        </div>
        <div className="dash-stat">
          <div className="dash-stat-label">Unterlagen</div>
          <div className="dash-stat-value">{dokDone} / {dokTotal}</div>
          <div className="dash-stat-bar"><span style={{ width: `${dokPct}%`, background: dokPct >= 100 ? 'var(--success)' : 'var(--vl-blue)' }} /></div>
        </div>
        <div className="dash-stat">
          <div className="dash-stat-label">Offene Aufgaben</div>
          <div className="dash-stat-value">{openTasks == null ? '…' : openTasks}</div>
          <div className="dash-stat-sub">{nextDue ? `nächste ${formatDate(nextDue)}` : (openTasks === 0 ? 'alles erledigt' : 'ohne Frist')}</div>
        </div>
        <div className="dash-stat">
          <div className="dash-stat-label">Ortstermin</div>
          <div className="dash-stat-value" style={!ortDate ? { color: 'var(--text-tertiary)' } : undefined}>{ortDate || '—'}</div>
          <div className="dash-stat-sub">{ortDate ? 'geplant' : 'noch offen'}</div>
        </div>
      </div>
    </div>
  );
};

const DashboardView = ({ p, onOpenAuftrag, onOpenGutachten, onOpenUpload, session, workerUrl, onRefresh, orgMembers }) => {
  const [viewerDokId, setViewerDokId] = useState(null);
  const viewerActive = !!viewerDokId;

  const handleOpenInline = useCallback(async (dokumentId) => {
    if (window.innerWidth < 768) {
      try {
        const res = await fetch(
          `${workerUrl}/api/document-url?id=${encodeURIComponent(dokumentId)}`,
          { headers: { Authorization: `Bearer ${session.access_token}` } }
        );
        if (res.ok) { const { url } = await res.json(); window.open(url, '_blank'); return; }
      } catch {}
    }
    setViewerDokId(dokumentId);
    if (typeof window.scrollTo === 'function') {
      window.scrollTo({ top: 0, behavior: 'smooth' });
    }
  }, [p.dokumente, session, workerUrl]);

  const mainContent = (
    <>
      <DashboardHero p={p} session={session} workerUrl={workerUrl} onRefresh={onRefresh} />

      {/* Auftrag + Beteiligte */}
      <div className="dashboard-grid two-col">
        <AuftragKachel p={p} onOpen={onOpenAuftrag} />
        <BeteiligteKachel p={p} onOpen={onOpenAuftrag} />
      </div>

      <AktivitaetenPanel
        projektId={p.id}
        beteiligte={p.beteiligte}
        session={session}
        workerUrl={workerUrl}
      />

      {/* ── Offene Dokumentenanfragen ── */}
      {(() => {
        const offeneAnfragen = (p.dokumente || []).filter(d =>
          d.angefragt_am && !d.eingetroffen_am
        );
        if (offeneAnfragen.length === 0) return null;
        const ueberfaellig = offeneAnfragen.filter(d =>
          d.frist_bis && daysUntil(d.frist_bis) < 0
        );
        return (
          <div className="card" style={{ marginBottom: 'var(--space-4)' }}>
            <div className="card-header" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <span className="card-title" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <IconClock size={16} stroke="var(--vl-blue)" />
                Offene Anfragen ({offeneAnfragen.length})
              </span>
              {ueberfaellig.length > 0 && (
                <span style={{
                  fontSize: 11, fontWeight: 700,
                  background: 'var(--danger-bg, rgba(220,38,38,0.06))', color: 'var(--danger)',
                  padding: '2px 8px', borderRadius: 10,
                }}>
                  {ueberfaellig.length} überfällig
                </span>
              )}
            </div>
            <div style={{ padding: '0 var(--space-4) var(--space-3)' }}>
              {offeneAnfragen
                .sort((a, b) => {
                  const aOver = a.frist_bis && daysUntil(a.frist_bis) < 0 ? 0 : 1;
                  const bOver = b.frist_bis && daysUntil(b.frist_bis) < 0 ? 0 : 1;
                  if (aOver !== bOver) return aOver - bOver;
                  if (a.frist_bis && b.frist_bis) return a.frist_bis.localeCompare(b.frist_bis);
                  return (a.frist_bis ? 0 : 1) - (b.frist_bis ? 0 : 1);
                })
                .map(d => {
                  const fristStatus = getDokFristStatus(d);
                  return (
                    <div key={d.id} style={{
                      display: 'flex', alignItems: 'center', gap: 10,
                      padding: '8px 0',
                      borderBottom: '1px solid var(--border-light)',
                      fontSize: 13,
                    }}>
                      <span style={{ flex: 1, fontWeight: 500 }}>{d.label || d.typ}</span>
                      {d.angefragt_am && (
                        <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                          {formatDate(d.angefragt_am)}
                        </span>
                      )}
                      {d.frist_bis && (
                        <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                          Frist: {formatDate(d.frist_bis)}
                        </span>
                      )}
                      {fristStatus && (
                        <span style={{
                          fontSize: 10, fontWeight: 600,
                          padding: '2px 8px', borderRadius: 10,
                          background: fristStatus.bg, color: fristStatus.color,
                          whiteSpace: 'nowrap',
                        }}>
                          {fristStatus.label}
                        </span>
                      )}
                    </div>
                  );
                })}
            </div>
          </div>
        );
      })()}

      <div className="gutachten-card-wrap">
        <div className="gutachten-card-wrap-header">
          <span className="gutachten-card-wrap-title">
            Gutachten ({p.gutachten.length})
          </span>
          <ActionButton
            className="btn btn-secondary btn-sm"
            onClick={async () => {
              try {
                const sortOrder = p.gutachten.length;
                const created = await apiInsertRow('gutachten', {
                  project_id: p.id,
                  titel: `Gutachten ${sortOrder + 1}`,
                  sort_order: sortOrder,
                }, session, workerUrl);
                if (created?.row?.id) {
                  await apiInsertRow('bewertungsobjekte', {
                    gutachten_id: created.row.id,
                    bezeichnung: 'Objekt 1',
                    sort_order: 0,
                  }, session, workerUrl);
                }
                onRefresh();
              } catch (e) {
                alert('Fehler: ' + (e.message || e));
                throw e;
              }
            }}
          >+ Weiteres Gutachten</ActionButton>
        </div>
        <div className="gutachten-grid">
          {p.gutachten.map((g, i) => (
            <GutachtenCard key={g.id || i} gutachten={g} idx={i} onOpen={onOpenGutachten}
              onDelete={async (gutachtenId) => {
                try {
                  const res = await fetch(`${workerUrl}/api/gutachten?id=${gutachtenId}`, {
                    method: 'DELETE',
                    headers: { Authorization: `Bearer ${session.access_token}` },
                  });
                  if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Fehler');
                  const data = await res.json();
                  if (data.projectDeleted) {
                    // Letztes Gutachten → zurück zur Liste
                    if (typeof onRefresh === 'function') onRefresh();
                  } else {
                    onRefresh();
                  }
                } catch (e) {
                  alert('Löschen fehlgeschlagen: ' + e.message);
                }
              }}
              projectId={p.id} session={session} workerUrl={workerUrl} onRefresh={onRefresh} />
          ))}
        </div>
      </div>

      {/* Dokumente-Panel im Dashboard ausgeblendet — Inhalte sind unter "Unterlagen" verfügbar */}
    </>
  );

  if (viewerActive) {
    return (
      <div className="view-wrapper">
        <div style={{
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          gap: 'var(--space-5)',
          alignItems: 'start',
        }}>
          <div>{mainContent}</div>
          <div style={{ position: 'sticky', top: 'var(--space-4)' }}>
            <DokumentViewer
              dokumente={p.dokumente || []}
              session={session}
              workerUrl={workerUrl}
              activeId={viewerDokId}
              onChangeActive={setViewerDokId}
            />
          </div>
        </div>
      </div>
    );
  }

  return <div className="view-wrapper">{mainContent}</div>;
};

// ══════════════════════════════════════════════════════════════════
// 4.3 · VIEW: AUFTRAG
// ══════════════════════════════════════════════════════════════════

const ObjTable = ({ children }) => <div className="obj-table">{children}</div>;

// ──────────────────────────────────────────────────────────────────
// StammdatenSektion — aufklappbare Sektion mit Vollständigkeits-Badge
// fields: Array von Werten (zum Zählen), children: ObjTableRow-Elemente
// ──────────────────────────────────────────────────────────────────
// Kompakte Liniensymbole je Datengruppe (rein dekorativ, ordnen optisch zu)
const SECTION_ICON_PATHS = {
  'Bewertung': 'M12 1v22 M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6',
  'Gebäude': 'M3 21h18 M5 21V8l7-4 7 4v13 M9.5 21v-5h5v5',
  'Grundstück': 'M3 6l6-3 6 3 6-3v15l-6 3-6-3-6 3z M9 3v15 M15 6v15',
  'Eigentum (WEG)': 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6',
  'Planungsrecht': 'M3 3h18v18H3z M3 9h18 M9 21V9',
  'Bodenwert': 'M12 2l9 5-9 5-9-5z M3 12l9 5 9-5 M3 17l9 5 9-5',
  'Energie': 'M13 2L3 14h7v8l10-12h-7z',
  'Auftragdaten': 'M6 2h9l5 5v15H6z M15 2v5h5 M9 13h6 M9 17h4',
  'Fristen & Termine': 'M3 4h18v18H3z M3 10h18 M8 2v4 M16 2v4',
  'Verfahren': 'M12 3v18 M5 7h14 M5 7l-2 6a3 3 0 0 0 6 0z M19 7l-2 6a3 3 0 0 0 6 0z M8 21h8',
  'Bearbeiter': 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2 M12 11a4 4 0 0 0 0-8 4 4 0 0 0 0 8z',
  'Nachgelagerte Schritte': 'M4 21V4 M4 4h11l-2 4 2 4H4',
};

const StammdatenSektion = ({ title, fields, children, defaultOpen, openSignal = 0 }) => {
  const filled = (fields || []).filter(f => f != null && f !== '' && f !== 0).length;
  const total = (fields || []).length;
  const pct = total > 0 ? filled / total : 0;
  const hasData = filled > 0;
  const [open, setOpen] = useState(defaultOpen != null ? defaultOpen : false);
  // Von außen aufklappen (Sprung aus „Noch offen" in eine eingeklappte Gruppe)
  const openSigRef = useRef(0);
  useEffect(() => {
    if (openSignal && openSignal !== openSigRef.current) { openSigRef.current = openSignal; setOpen(true); }
  }, [openSignal]);

  const color = pct >= 1 ? 'var(--success)' : pct > 0 ? 'var(--warning)' : 'var(--text-tertiary)';
  const complete = total > 0 && filled >= total;
  const chipBg = complete ? 'var(--success-bg)' : pct > 0 ? 'var(--warning-bg)' : 'var(--surface-light)';
  const chipColor = complete ? 'var(--success)' : pct > 0 ? 'var(--warning)' : 'var(--text-tertiary)';
  const iconPath = SECTION_ICON_PATHS[title] || 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z';
  const R = 9, CIRC = 2 * Math.PI * R;

  return (
    <div style={{
      marginBottom: 'var(--space-3)', background: 'var(--surface)',
      border: '1px solid var(--border-light)', borderRadius: 'var(--radius-lg)',
      boxShadow: 'var(--shadow-subtle)', overflow: 'hidden',
    }}>
      <button type="button" onClick={() => setOpen(!open)} style={{
        display: 'flex', alignItems: 'center', gap: 'var(--space-3)', width: '100%',
        padding: '13px 16px', background: open ? 'var(--surface-raised)' : 'var(--surface)',
        border: 'none', cursor: 'pointer', textAlign: 'left',
        transition: 'background var(--dur-fast) var(--ease-out)',
      }}>
        <span style={{
          width: 34, height: 34, borderRadius: 'var(--radius-md)', flexShrink: 0,
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          background: chipBg, color: chipColor,
          transition: 'background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out)',
        }}>
          <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
            <path d={iconPath} />
          </svg>
        </span>
        <span style={{ flex: 1, minWidth: 0 }}>
          <span style={{ display: 'block', fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>{title}</span>
          <span style={{ display: 'block', fontSize: 12, color: 'var(--text-tertiary)', marginTop: 1 }}>
            {complete ? 'Vollständig erfasst' : (total > 0 && filled === 0) ? 'Noch nichts erfasst' : total > 0 ? `${filled} von ${total} erfasst` : 'Keine Felder'}
          </span>
        </span>
        <span style={{ position: 'relative', width: 26, height: 26, flexShrink: 0 }} title={`${filled}/${total}`}>
          <svg width="26" height="26" viewBox="0 0 26 26" style={{ transform: 'rotate(-90deg)' }}>
            <circle cx="13" cy="13" r={R} fill="none" stroke="var(--border-light)" strokeWidth="3" />
            <circle cx="13" cy="13" r={R} fill="none" stroke={color} strokeWidth="3" strokeLinecap="round"
              strokeDasharray={CIRC} strokeDashoffset={CIRC * (1 - pct)}
              style={{ transition: 'stroke-dashoffset var(--dur-slow) var(--ease-out), stroke var(--dur-base)' }} />
          </svg>
          {complete && (
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--success)" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round"
              style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)' }}>
              <polyline points="20 6 9 17 4 12" />
            </svg>
          )}
        </span>
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
          style={{ color: 'var(--text-tertiary)', flexShrink: 0, transform: open ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform var(--dur-base) var(--ease-out)' }}>
          <polyline points="9 18 15 12 9 6" />
        </svg>
      </button>
      <div style={{ display: 'grid', gridTemplateRows: open ? '1fr' : '0fr', transition: 'grid-template-rows var(--dur-base) var(--ease-out)' }}>
        <div style={{ overflow: 'hidden', minHeight: 0 }}>
          <div style={{ padding: '0 14px 14px' }}>
            <ObjTable>{children}</ObjTable>
          </div>
        </div>
      </div>
    </div>
  );
};

const ObjTableRow = ({ label, children, herkunft, onOpenDocument, onOpenInline, sourceHint, id }) => (
  <div className="obj-row" id={id}>
    <div className="obj-label">
      {label}
      {sourceHint && !herkunft && (
        <div style={{ fontSize: 10, color: 'var(--text-tertiary)', fontWeight: 400, marginTop: 1, opacity: 0.7 }}>
          ← {sourceHint}
        </div>
      )}
    </div>
    <div className="obj-value" style={{ justifyContent: 'space-between' }}>
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
        {children}
      </span>
      {herkunft && (onOpenDocument || onOpenInline) && (
        <ProvenanceBadge
          herkunft={herkunft}
          onOpen={onOpenDocument}
          onOpenInline={onOpenInline}
        />
      )}
    </div>
  </div>
);

// Dropdowns für Enum-Felder in AuftragView und anderswo
const AUFTRAGSART_OPTIONS = [
  { value: 'privat', label: 'Privatauftrag' },
  { value: 'gericht', label: 'Gerichtsauftrag' },
  { value: 'gutachterausschuss', label: 'Gutachterausschuss' },
  { value: 'notariat', label: 'Notariat' },
];
const AUFTRAGSTYP_OPTIONS = [
  { value: 'verkehrswert', label: 'Verkehrswertgutachten' },
  { value: 'miete', label: 'Mietwertgutachten' },
  { value: 'beleihung', label: 'Beleihungswertgutachten' },
  { value: 'marktwert', label: 'Marktwertindikation' },
  { value: 'minderwert', label: 'Minderwertgutachten' },
  { value: 'ueberwachung', label: 'Überwachungsgutachten' },
];
const GERICHTSTYP_OPTIONS = [
  { value: 'amtsgericht', label: 'Amtsgericht' },
  { value: 'landgericht', label: 'Landgericht' },
  { value: 'oberlandesgericht', label: 'Oberlandesgericht' },
];

const EFFIZIENZKLASSE_OPTIONS = [
  { value: 'A+', label: 'A+' }, { value: 'A', label: 'A' },
  { value: 'B', label: 'B' }, { value: 'C', label: 'C' },
  { value: 'D', label: 'D' }, { value: 'E', label: 'E' },
  { value: 'F', label: 'F' }, { value: 'G', label: 'G' },
  { value: 'H', label: 'H' },
];

const ENERGIEAUSWEIS_TYP_OPTIONS = [
  { value: 'bedarfsausweis', label: 'Bedarfsausweis' },
  { value: 'verbrauchsausweis', label: 'Verbrauchsausweis' },
];

// ────────────────────────────────────────────────────────────────────
// ZeitenKostenPanel — Zeiterfassung + Kostenübersicht je Auftrag.
// Stundensatz × erfasste Zeit ergibt die aufgelaufenen Kosten; sobald
// sie den Kostenvorschuss übersteigen, erscheint ein Überlauf-Hinweis.
// ────────────────────────────────────────────────────────────────────
const fmtStunden = (min) => {
  const h = (min || 0) / 60;
  return h.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
};
const fmtEuro = (n) => '€' + (Math.round((n || 0) * 100) / 100).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 });

const ZeitenKostenPanel = ({ p, updateAuftragField, session, workerUrl }) => {
  const [entries, setEntries] = useState(null);
  const [addOpen, setAddOpen] = useState(false);
  const [form, setForm] = useState(() => ({ datum: new Date().toISOString().slice(0, 10), stunden: '', beschreibung: '' }));
  const [formErr, setFormErr] = useState(null);
  const [deletingId, setDeletingId] = useState(null);
  const isMobile = useIsMobile();

  const load = useCallback(async () => {
    if (!p.id) return;
    try {
      const res = await fetch(`${workerUrl}/api/zeiterfassung?project_id=${encodeURIComponent(p.id)}`, {
        headers: { Authorization: `Bearer ${session.access_token}` },
      });
      const data = await res.json();
      setEntries(Array.isArray(data.zeiterfassung) ? data.zeiterfassung : []);
    } catch (e) { setEntries([]); }
  }, [p.id, session, workerUrl]);
  useEffect(() => { load(); }, [load]);

  const rate = p.vereinbarterStundensatz;
  const vorschuss = p.kostenvorschuss;
  const totalMin = (entries || []).reduce((s, e) => s + (e.dauer_minuten || 0), 0);
  const totalHours = totalMin / 60;
  const accrued = rate != null ? totalHours * rate : null;
  const hasBudget = vorschuss != null && vorschuss > 0;
  const ratio = (hasBudget && accrued != null) ? accrued / vorschuss : null;
  const overflow = (hasBudget && accrued != null) ? accrued - vorschuss : null;
  const isOverflow = overflow != null && overflow > 0;
  const barColor = isOverflow ? 'var(--danger)' : (ratio != null && ratio > 0.85 ? 'var(--warning)' : 'var(--success)');

  const handleCreate = async () => {
    const hours = parseFloat(String(form.stunden).replace(',', '.'));
    if (!isFinite(hours) || hours <= 0) { setFormErr('Bitte gültige Stunden eingeben (z. B. 1,5).'); throw new Error('invalid'); }
    setFormErr(null);
    await apiInsertRow('zeiterfassung', {
      project_id: p.id,
      datum: form.datum || null,
      dauer_minuten: Math.round(hours * 60),
      beschreibung: form.beschreibung || null,
    }, session, workerUrl);
    await load();
  };

  const handleDelete = async (id) => {
    setDeletingId(id);
    try {
      await apiDeleteRow('zeiterfassung', id, session, workerUrl);
      setEntries(prev => (prev || []).filter(e => e.id !== id));
    } catch (e) { alert('Löschen fehlgeschlagen: ' + e.message); }
    finally { setDeletingId(null); }
  };

  const labelStyle = { display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 4 };
  const inputStyle = { padding: '8px 10px', width: '100%', boxSizing: 'border-box', border: '1px solid var(--border-medium)', borderRadius: 'var(--radius-sm)', fontSize: 14, background: 'var(--surface)' };

  return (
    <div style={{ marginBottom: 'var(--space-8)' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, margin: '0 0 var(--space-3)' }}>
        <h3 style={{ fontSize: 17, fontWeight: 700, color: 'var(--text-primary)', margin: 0 }}>Zeiten &amp; Kosten</h3>
        <span style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>
          {fmtStunden(totalMin)} h erfasst{accrued != null ? ` · ${fmtEuro(accrued)}` : ''}
        </span>
      </div>

      {/* Konditionen + Kostenübersicht */}
      <div className="card" style={{ padding: 'var(--space-5)', marginBottom: 'var(--space-3)' }}>
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 'var(--space-3)' : 'var(--space-5)', marginBottom: 'var(--space-4)' }}>
          <div>
            <span style={labelStyle}>Vereinbarter Stundensatz</span>
            <EditableField
              value={rate}
              type="number"
              placeholder="—"
              display={rate != null ? `${fmtEuro(rate)} / h` : null}
              onSave={(v) => updateAuftragField('vereinbarter_stundensatz', v)}
            />
          </div>
          <div>
            <span style={labelStyle}>Kostenvorschuss</span>
            <EditableField
              value={vorschuss}
              type="number"
              placeholder="—"
              display={vorschuss != null ? fmtEuro(vorschuss) : null}
              onSave={(v) => updateAuftragField('kostenvorschuss', v)}
            />
          </div>
        </div>

        {rate == null ? (
          <div style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>
            Stundensatz festlegen, um aufgelaufene Kosten zu berechnen.
          </div>
        ) : (
          <div>
            <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, marginBottom: 6 }}>
              <span style={{ color: 'var(--text-secondary)' }}>
                Aufgelaufen: <strong style={{ color: isOverflow ? 'var(--danger)' : 'var(--text-primary)' }}>{fmtEuro(accrued)}</strong>
              </span>
              {hasBudget && (
                <span style={{ color: 'var(--text-tertiary)' }}>
                  {isOverflow ? `${fmtEuro(Math.abs(overflow))} über Vorschuss` : `${fmtEuro(vorschuss - accrued)} verbleibend`}
                </span>
              )}
            </div>
            {hasBudget && (
              <div style={{ height: 8, borderRadius: 4, background: 'var(--border-light)', overflow: 'hidden' }}>
                <div style={{ height: '100%', width: `${Math.min(100, (ratio || 0) * 100)}%`, background: barColor, transition: 'width .4s var(--ease-out)' }} />
              </div>
            )}
            {isOverflow && (
              <div style={{ marginTop: 'var(--space-3)', padding: '8px 12px', borderRadius: 'var(--radius-sm)', background: 'var(--danger-bg)', color: 'var(--danger)', fontSize: 13, fontWeight: 500 }}>
                Überlauf: Die aufgelaufenen Kosten übersteigen den Kostenvorschuss um {fmtEuro(overflow)}.
              </div>
            )}
          </div>
        )}
      </div>

      {/* Zeiteinträge */}
      <div className="card" style={{ padding: 'var(--space-4) var(--space-5)' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: entries && entries.length ? 'var(--space-3)' : 0 }}>
          <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)' }}>
            Zeiteinträge{entries ? ` (${entries.length})` : ''}
          </span>
          {!addOpen && (
            <button type="button" className="btn btn-secondary btn-sm" onClick={() => { setFormErr(null); setAddOpen(true); }}>
              + Zeit erfassen
            </button>
          )}
        </div>

        {addOpen && (
          <div style={{ background: 'var(--surface-light)', borderRadius: 'var(--radius-md)', padding: 'var(--space-4)', marginBottom: entries && entries.length ? 'var(--space-3)' : 0 }}>
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr 1fr' : '140px 110px 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
              <div>
                <span style={labelStyle}>Datum</span>
                <input type="date" style={inputStyle} value={form.datum} onChange={(e) => setForm(f => ({ ...f, datum: e.target.value }))} />
              </div>
              <div>
                <span style={labelStyle}>Stunden</span>
                <input type="number" step="0.25" min="0" placeholder="1,5" style={inputStyle} value={form.stunden} onChange={(e) => setForm(f => ({ ...f, stunden: e.target.value }))} />
              </div>
              <div style={{ gridColumn: isMobile ? '1 / -1' : 'auto' }}>
                <span style={labelStyle}>Beschreibung</span>
                <input type="text" placeholder="z. B. Ortstermin, Recherche" style={inputStyle} value={form.beschreibung} onChange={(e) => setForm(f => ({ ...f, beschreibung: e.target.value }))} />
              </div>
            </div>
            {formErr && <div style={{ fontSize: 12, color: 'var(--danger)', marginBottom: 'var(--space-2)' }}>{formErr}</div>}
            <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
              <ActionButton className="btn btn-primary btn-sm action-btn" onClick={handleCreate}
                onDone={() => { setForm({ datum: new Date().toISOString().slice(0, 10), stunden: '', beschreibung: '' }); setAddOpen(false); }}>
                Erfassen
              </ActionButton>
              <button type="button" className="btn btn-ghost btn-sm" onClick={() => { setAddOpen(false); setFormErr(null); }}>Abbrechen</button>
            </div>
          </div>
        )}

        {entries === null ? (
          <div style={{ fontSize: 13, color: 'var(--text-tertiary)', padding: '4px 0' }}>Lade Zeiteinträge…</div>
        ) : entries.length === 0 ? (
          !addOpen && <div style={{ fontSize: 13, color: 'var(--text-tertiary)', padding: '4px 0' }}>Noch keine Zeiten erfasst.</div>
        ) : (
          <div style={{ display: 'flex', flexDirection: 'column' }}>
            {entries.map((e, i) => (
              <div key={e.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '9px 0', borderTop: i === 0 ? 'none' : '1px solid var(--border-light)', opacity: deletingId === e.id ? 0.5 : 1 }}>
                <span style={{ fontSize: 13, color: 'var(--text-tertiary)', width: isMobile ? 64 : 84, flexShrink: 0 }}>{e.datum ? formatDate(e.datum) : '—'}</span>
                <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', width: isMobile ? 52 : 64, flexShrink: 0 }}>{fmtStunden(e.dauer_minuten)} h</span>
                <span style={{ flex: 1, minWidth: 0, fontSize: 14, color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.beschreibung || '—'}</span>
                {rate != null && !isMobile && (
                  <span style={{ fontSize: 13, color: 'var(--text-tertiary)', flexShrink: 0 }}>{fmtEuro((e.dauer_minuten || 0) / 60 * rate)}</span>
                )}
                <button type="button" className="btn btn-ghost btn-sm" title="Eintrag entfernen" disabled={deletingId === e.id}
                  onClick={() => handleDelete(e.id)} style={{ flexShrink: 0, padding: '2px 8px', color: 'var(--text-tertiary)' }}>×</button>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

const AuftragView = ({ p, herkunft, onOpenDocument, session, workerUrl, onRefresh, orgMembers }) => {
  const showToast = useToast();
  // Inline-Viewer-State
  const [viewerDokId, setViewerDokId] = useState(null);
  const viewerActive = !!viewerDokId;

  // Scope-Filter: nur Dokumente, die zum Auftrag gehören
  const auftragDokumente = useMemo(
    () => (p.dokumente || []).filter(d => d.scope === 'auftrag'),
    [p.dokumente]
  );

  // Helfer: Herkunft für ein Feld in auftraege-Tabelle
  const h = (feld) => {
    if (!p.auftrag_id) return null;
    return herkunft?.[`auftraege:${p.auftrag_id}:${feld}`];
  };

  // Klick auf Provenance-Badge → Dokument rechts einblenden (Desktop) oder im neuen Tab (Mobile)
  const handleOpenInline = useCallback(async (dokumentId) => {
    if (window.innerWidth < 768) {
      try {
        const res = await fetch(
          `${workerUrl}/api/document-url?id=${encodeURIComponent(dokumentId)}`,
          { headers: { Authorization: `Bearer ${session.access_token}` } }
        );
        if (res.ok) { const { url } = await res.json(); window.open(url, '_blank'); return; }
      } catch {}
    }
    setViewerDokId(dokumentId);
    if (typeof window.scrollTo === 'function') {
      window.scrollTo({ top: 0, behavior: 'smooth' });
    }
  }, [p.dokumente, session, workerUrl]);

  // Helfer: Feld im Auftrag via /api/row aktualisieren, dann Projekt neu laden.
  // Numerische Felder bekommen Number() übergeben, null bleibt null.
  const updateAuftragField = async (feld, wert) => {
    if (!p.auftrag_id) throw new Error('Auftrag noch nicht angelegt');
    const normalized =
      (feld === 'kostenvorschuss' || feld === 'ausfertigungen' || feld === 'vereinbarter_stundensatz')
        ? (wert == null || wert === '' ? null : Number(wert))
        : wert;
    const patch = { [feld]: normalized };
    // Auto-Default: Abgabe intern = 1 Monat vor Abgabe extern (wenn intern noch leer)
    if (feld === 'abgabe_extern' && normalized && !p.abgabeIntern) {
      const d = new Date(normalized);
      d.setMonth(d.getMonth() - 1);
      patch.abgabe_intern = d.toISOString().split('T')[0];
    }
    await apiPatchRow('auftraege', p.auftrag_id, patch, session, workerUrl);

    // ── SV-Wechsel → internes AZ-Kürzel automatisch anpassen ──
    // Das interne AZ (projects.name) hat das Format "nr/jahr/kürzel" (z.B. 12/26/LV).
    // Wird der Sachverständige geändert, soll nur das Kürzel am Ende getauscht
    // werden — Nummer und Jahr bleiben unverändert (kein Neu-Generieren!).
    if (feld === 'sachverstaendiger' && normalized) {
      const sv = (orgMembers || []).find(m => m.name === normalized);
      const neuesKuerzel = sv?.akte_kuerzel;
      const aktuellesAz = p.name || '';
      // Nur anpassen, wenn der gewählte SV ein Kürzel hat und das AZ dem
      // erwarteten Format "…/kürzel" entspricht.
      const azMatch = aktuellesAz.match(/^(.+\/)([A-Za-zÄÖÜäöü]+)$/);
      if (neuesKuerzel && azMatch && azMatch[2] !== neuesKuerzel) {
        const neuesAz = azMatch[1] + neuesKuerzel;
        try {
          await apiPatchRow('projects', p.id, { name: neuesAz }, session, workerUrl);
          showToast(`Aktenzeichen angepasst: ${neuesAz}`, 'success');
        } catch (e) {
          console.warn('[AZ] Kürzel-Anpassung fehlgeschlagen:', e.message);
        }
      }
    }

    onRefresh && onRefresh();
  };
  const canEdit = !!(session);

  // ── Steckbrief: Objektfoto (erstes Gutachten), Status, kritische Fristen ──
  const titelGutachten = (p.gutachten || [])[0] || null;
  const [heroImgUrl, setHeroImgUrl] = useState(null);
  useEffect(() => {
    const path = titelGutachten?.titelbild_path;
    if (!path || !session?.access_token || !workerUrl) { setHeroImgUrl(null); return; }
    let cancelled = false;
    fetch(`${workerUrl}/api/signed-url?path=${encodeURIComponent(path)}`, {
      headers: { Authorization: `Bearer ${session.access_token}` },
    })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (!cancelled && d?.url) setHeroImgUrl(d.url); })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [titelGutachten?.titelbild_path, session, workerUrl]);

  // Titelbild direkt aus dem Steckbrief setzen/ändern (skaliert, wie in der früheren Kopfleiste)
  const heroFileRef = useRef(null);
  const [heroUploading, setHeroUploading] = useState(false);
  const handleHeroUpload = async (e) => {
    const file = e.target?.files?.[0];
    if (!file || !titelGutachten?.id || !session?.access_token) return;
    setHeroUploading(true);
    try {
      const blob = await new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
          const maxW = 800;
          const scale = img.width > maxW ? maxW / img.width : 1;
          const c = document.createElement('canvas');
          c.width = img.width * scale; c.height = img.height * scale;
          c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
          c.toBlob(b => resolve(b), 'image/jpeg', 0.85);
        };
        img.src = URL.createObjectURL(file);
      });
      const fd = new FormData();
      fd.append('file', blob, 'titelbild.jpg');
      fd.append('project_id', p.id);
      fd.append('gutachten_id', titelGutachten.id);
      const upRes = await fetch(`${workerUrl}/api/photo-upload`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${session.access_token}` },
        body: fd,
      });
      if (!upRes.ok) throw new Error('Upload fehlgeschlagen');
      const { storage_path } = await upRes.json();
      await apiPatchRow('gutachten', titelGutachten.id, { titelbild_path: storage_path }, session, workerUrl);
      if (onRefresh) onRefresh();
    } catch (err) {
      console.error('Titelbild-Upload:', err);
    } finally {
      setHeroUploading(false);
      if (heroFileRef.current) heroFileRef.current.value = '';
    }
  };

  const statusInfo = AUFTRAG_STATUS_BY_VALUE[p.bearbeitungsstatus || 'angelegt'];
  const istGericht = p.auftragsart_raw === 'gericht';

  // Ampel-Status für eine Frist (überfällig/heute/bald/später)
  const fristAmpel = (iso) => {
    const dd = daysUntil(iso);
    if (dd == null) return { color: 'var(--text-tertiary)', bg: 'var(--surface-light)', rel: 'kein Datum' };
    if (dd < 0) return { color: 'var(--danger)', bg: 'rgba(184,55,31,0.10)', rel: `überfällig · ${Math.abs(dd)} ${Math.abs(dd) === 1 ? 'Tag' : 'Tage'}` };
    if (dd === 0) return { color: 'var(--danger)', bg: 'rgba(184,55,31,0.10)', rel: 'heute fällig' };
    if (dd <= 7) return { color: 'var(--warning)', bg: 'rgba(196,117,0,0.10)', rel: `in ${dd} ${dd === 1 ? 'Tag' : 'Tagen'}` };
    return { color: 'var(--success)', bg: 'rgba(13,122,79,0.09)', rel: `in ${dd} Tagen` };
  };

  // Zeitkritische Termine; bei Gerichtsauftrag zusätzlich Stellungnahme/Anhörung (nur wenn gesetzt)
  // Projekt liefert Fristen teils formatiert (dd.mm.yyyy) → für daysUntil nach ISO
  const toIso = (v) => {
    if (!v) return null;
    const m = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(v);
    return m ? `${m[3]}-${m[2]}-${m[1]}` : v;
  };
  const fristen = [
    { label: 'Abgabe extern', iso: toIso(p.fristExtern) },
    { label: 'Abgabe intern', iso: toIso(p.fristIntern) },
    ...(istGericht ? [
      { label: 'Stellungnahme 1', iso: toIso(p.frist_stellungnahme_1) },
      { label: 'Anhörung 1', iso: toIso(p.anhoerung_1_vom) },
    ].filter(f => f.iso) : []),
  ];

  // Schlüsselfelder des Auftrags — „Noch offen", anklickbar (Sprung + Öffnen)
  const [feldOpen, setFeldOpen] = useState({ key: null, nonce: 0 });
  const springeZuFeld = (key) => {
    setFeldOpen({ key, nonce: Date.now() });
    // kurze Verzögerung, damit eine eingeklappte Karte zuerst aufgeht
    setTimeout(() => {
      const el = document.getElementById('auftrag-feld-' + key);
      if (el) {
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
        el.classList.remove('feld-pulse'); void el.offsetWidth; el.classList.add('feld-pulse');
        setTimeout(() => el.classList.remove('feld-pulse'), 1500);
      }
    }, 280);
  };
  const offeneFelder = [
    { key: 'auftraggeber', label: 'Auftraggeber', leer: !p.auftraggeber },
    { key: 'abgabe_extern', label: 'Abgabe extern', leer: !p.fristExtern },
    { key: 'zweck', label: 'Zweck / Verfahren', leer: !(p.verfahrensart || p.zweck) },
    { key: 'sachverstaendiger', label: 'Sachverständiger', leer: !p.sachverstaendiger },
  ].filter(f => f.leer);

  const dataColumn = (
    <>
      {/* ── Steckbrief-Hero: Objektfoto · Identität · Frist-Ampel ── */}
      <div className="card" style={{ marginBottom: 'var(--space-5)', padding: 0, overflow: 'hidden' }}>
        <div style={{ display: 'flex', gap: 'var(--space-5)', padding: 'var(--space-5)', flexWrap: 'wrap', alignItems: 'flex-start' }}>
          <div
            onClick={() => titelGutachten?.id && heroFileRef.current?.click()}
            onMouseEnter={(e) => { const ov = e.currentTarget.querySelector('.hero-img-overlay'); if (ov) ov.style.opacity = '1'; }}
            onMouseLeave={(e) => { const ov = e.currentTarget.querySelector('.hero-img-overlay'); if (ov) ov.style.opacity = '0'; }}
            title={titelGutachten?.id ? (heroImgUrl ? 'Objektfoto ändern' : 'Objektfoto hinzufügen') : undefined}
            style={{ width: 180, height: 120, flexShrink: 0, borderRadius: 'var(--radius-md)', overflow: 'hidden', background: 'var(--surface-light)', border: heroImgUrl ? '1px solid var(--border-light)' : '2px dashed var(--border-light)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: titelGutachten?.id ? 'pointer' : 'default', position: 'relative' }}>
            {heroImgUrl ? (
              <>
                <img src={heroImgUrl} alt="Objektfoto" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                <div className="hero-img-overlay" style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', opacity: 0, transition: 'opacity 0.2s', color: '#fff', fontSize: 13, fontWeight: 600 }}>Ändern</div>
              </>
            ) : (
              <div style={{ textAlign: 'center', padding: 8 }}>
                <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="1.5"><rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /></svg>
                <div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 4 }}>{titelGutachten?.id ? 'Objektfoto hinzufügen' : 'Kein Objektfoto'}</div>
              </div>
            )}
            {heroUploading && (
              <div style={{ position: 'absolute', inset: 0, background: 'rgba(255,255,255,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>Lädt…</div>
            )}
            <input ref={heroFileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={handleHeroUpload} />
          </div>
          <div style={{ flex: 1, minWidth: 220, display: 'flex', flexDirection: 'column', gap: 6 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 22, fontWeight: 700, color: 'var(--text-primary)', letterSpacing: '-0.01em' }}>{p.name || 'Ohne Aktenzeichen'}</span>
              <span className={istGericht ? 'pill pill-blue' : 'pill'}>
                {auftragsartLabel(p.auftragsart_raw) || '—'}
              </span>
              {statusInfo && (
                <span className="pill" style={{ color: statusInfo.fg, background: statusInfo.bg }}>{statusInfo.short}</span>
              )}
            </div>
            {p.akte && <span style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Auftragsnummer {p.akte}</span>}
            {titelGutachten?.adresse && <span style={{ fontSize: 15, color: 'var(--text-secondary)' }}>{titelGutachten.adresse}</span>}
            {p.auftraggeber && <span style={{ fontSize: 14, color: 'var(--text-secondary)' }}>Auftraggeber: {p.auftraggeber}</span>}
          </div>
        </div>
        <div style={{ borderTop: '1px solid var(--border-light)', padding: 'var(--space-4) var(--space-5)', background: 'var(--surface-light)' }}>
          <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-tertiary)', marginBottom: 'var(--space-2)' }}>Fristen</div>
          <div style={{ display: 'flex', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
            {fristen.map(f => {
              const s = fristAmpel(f.iso);
              return (
                <div key={f.label} style={{ display: 'flex', flexDirection: 'column', gap: 2, padding: '8px 12px', borderRadius: 'var(--radius-md)', background: s.bg, minWidth: 140 }}>
                  <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>{f.label}</span>
                  <span style={{ fontSize: 15, fontWeight: 700, color: s.color, fontVariantNumeric: 'tabular-nums' }}>{f.iso ? formatDate(f.iso) : '—'}</span>
                  <span style={{ fontSize: 11, fontWeight: 600, color: s.color }}>{s.rel}</span>
                </div>
              );
            })}
          </div>
          {offeneFelder.length > 0 && (
            <div style={{ marginTop: 'var(--space-4)' }}>
              <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-tertiary)', marginBottom: 'var(--space-2)' }}>Noch offen</div>
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                {offeneFelder.map(f => (
                  <button key={f.key} type="button" onClick={() => springeZuFeld(f.key)}
                    className="pill pill-warning" style={{ cursor: 'pointer' }}>
                    {f.label}
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>

      <StammdatenSektion title="Auftragdaten" fields={[p.auftraggeber, p.akte, p.auftragstyp, p.auftragsbeschreibung, (p.verfahrensart || p.zweck)]} openSignal={(feldOpen.key === 'auftraggeber' || feldOpen.key === 'zweck') ? feldOpen.nonce : 0}>
          <ObjTableRow label="Status">
            <EditableField
              value={p.bearbeitungsstatus || 'angelegt'}
              type="select"
              options={AUFTRAG_STATUS_OPTIONS}
              display={(() => {
                const st = AUFTRAG_STATUS_BY_VALUE[p.bearbeitungsstatus || 'angelegt'];
                if (!st) return p.bearbeitungsstatus || '—';
                return (
                  <span className="pill" style={{ color: st.fg, background: st.bg, whiteSpace: 'nowrap' }}>{st.label}</span>
                );
              })()}
              onSave={(v) => updateAuftragField('bearbeitungsstatus', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="AZ (intern)">
            <EditableField
              value={p.name}
              onSave={async (v) => {
                if (!p.id) throw new Error('Projekt-ID fehlt');
                if (!v || !v.trim()) throw new Error('Internes AZ darf nicht leer sein');
                await apiPatchRow('projects', p.id, { name: v.trim() }, session, workerUrl);
                onRefresh && onRefresh();
              }}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Auftragsnummer" herkunft={h('akte') || h('aktenzeichen')} onOpenInline={handleOpenInline}>
            <EditableField
              value={p.akte}
              onSave={(v) => updateAuftragField('akte', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Auftragsart" herkunft={h('auftragsart')} onOpenInline={handleOpenInline}>
            <EditableField
              value={p.auftragsart_raw}
              type="select"
              options={AUFTRAGSART_OPTIONS}
              display={p.auftragsart}
              onSave={(v) => updateAuftragField('auftragsart', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Auftragstyp" herkunft={h('auftragstyp')} onOpenInline={handleOpenInline}>
            <EditableField
              value={AUFTRAGSTYP_OPTIONS.find(o => o.label === p.auftragstyp)?.value ?? ''}
              type="select"
              options={AUFTRAGSTYP_OPTIONS}
              display={p.auftragstyp}
              onSave={(v) => updateAuftragField('auftragstyp', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Auftraggeber" herkunft={h('auftraggeber')} onOpenInline={handleOpenInline} id="auftrag-feld-auftraggeber">
            <EditableField
              value={p.auftraggeber}
              openSignal={feldOpen.key === 'auftraggeber' ? feldOpen.nonce : 0}
              onSave={(v) => updateAuftragField('auftraggeber', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Auftragsbeschreibung" herkunft={h('auftragsbeschreibung')} onOpenInline={handleOpenInline}>
            <EditableField
              value={p.auftragsbeschreibung || ''}
              placeholder="z.B. Ermittlung des Verkehrswertes nach § 194 BauGB …"
              onSave={(v) => updateAuftragField('auftragsbeschreibung', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Zweck / Verfahren" herkunft={h('verfahrensart') || h('zweck')} onOpenInline={handleOpenInline} id="auftrag-feld-zweck">
            <EditableField
              value={p.verfahrensart || p.zweck || ''}
              openSignal={feldOpen.key === 'zweck' ? feldOpen.nonce : 0}
              placeholder={p.auftragsart_raw === 'privat' ? 'Privater Auftrag' : '—'}
              onSave={(v) => updateAuftragField(
                p.auftragsart_raw === 'gericht' ? 'verfahrensart' : 'zweck',
                v
              )}
              disabled={!canEdit}
            />
          </ObjTableRow>
        </StammdatenSektion>

      <StammdatenSektion title={"Fristen & Termine"} fields={[p.auftragseingang, p.fristIntern, p.fristExtern, p.ortstermin, ...(istGericht ? [p.beschlussdatum] : [])]} openSignal={feldOpen.key === 'abgabe_extern' ? feldOpen.nonce : 0}>
          {p.auftragsart_raw === 'gericht' && (
            <ObjTableRow label="Beschlussdatum" herkunft={h('beschlussdatum')} onOpenInline={handleOpenInline}>
              <EditableField
                value={p.beschlussdatum && /\d{2}\.\d{2}\.\d{4}/.test(p.beschlussdatum)
                  ? p.beschlussdatum.split('.').reverse().join('-')
                  : p.beschlussdatum}
                type="date"
                display={p.beschlussdatum}
                onSave={(v) => updateAuftragField('beschlussdatum', v)}
                disabled={!canEdit}
              />
            </ObjTableRow>
          )}
          <ObjTableRow label="Auftragseingang" herkunft={h('auftragseingang')} onOpenInline={handleOpenInline}>
            <EditableField
              value={p.auftragseingang && /\d{2}\.\d{2}\.\d{4}/.test(p.auftragseingang)
                ? p.auftragseingang.split('.').reverse().join('-')
                : p.auftragseingang}
              type="date"
              display={p.auftragseingang}
              onSave={(v) => updateAuftragField('auftragseingang', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Abgabe intern">
            <EditableField
              value={p.fristIntern && /\d{2}\.\d{2}\.\d{4}/.test(p.fristIntern)
                ? p.fristIntern.split('.').reverse().join('-')
                : p.fristIntern}
              type="date"
              display={p.fristIntern}
              onSave={(v) => updateAuftragField('abgabe_intern', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Abgabe extern" herkunft={h('abgabe_extern')} onOpenInline={handleOpenInline} id="auftrag-feld-abgabe_extern">
            <EditableField
              value={p.fristExtern && /\d{2}\.\d{2}\.\d{4}/.test(p.fristExtern)
                ? p.fristExtern.split('.').reverse().join('-')
                : p.fristExtern}
              openSignal={feldOpen.key === 'abgabe_extern' ? feldOpen.nonce : 0}
              type="date"
              display={p.fristExtern ? (
                <>
                  <strong>{p.fristExtern}</strong>
                  {p.fristRestDays != null && (
                    <Pill variant="warning" style={{ fontSize: 11, marginLeft: 8 }}>{p.fristRestDays} Tage</Pill>
                  )}
                </>
              ) : null}
              onSave={(v) => updateAuftragField('abgabe_extern', v)}
              disabled={!canEdit}
            />
          </ObjTableRow>
          <ObjTableRow label="Ortstermin(e)">{p.ortstermin || <span className="obj-value-empty">—</span>}</ObjTableRow>
        </StammdatenSektion>

      {p.auftragsart_raw === 'gericht' && (
        <StammdatenSektion title="Verfahren" fields={[p.gerichtstyp, p.verfahrensart, p.sache, p.wegen, p.kostenvorschuss, p.ausfertigungen, p.belastungenAbt2]}>
            <ObjTableRow label="Gerichtstyp" herkunft={h('gerichtstyp')} onOpenInline={handleOpenInline}>
              <EditableField
                value={p.gerichtstyp}
                type="select"
                options={GERICHTSTYP_OPTIONS}
                display={p.gerichtstyp
                  ? (p.gerichtstyp.charAt(0).toUpperCase() + p.gerichtstyp.slice(1))
                  : null}
                onSave={(v) => updateAuftragField('gerichtstyp', v)}
                disabled={!canEdit}
              />
            </ObjTableRow>
            <ObjTableRow label="Art des Verfahrens" herkunft={h('verfahrensart')} onOpenInline={handleOpenInline}>
              <EditableField
                value={p.verfahrensart}
                onSave={(v) => updateAuftragField('verfahrensart', v)}
                disabled={!canEdit}
              />
            </ObjTableRow>
            <ObjTableRow label="Sache" herkunft={h('sache')} onOpenInline={handleOpenInline}>
              <EditableField
                value={p.sache}
                onSave={(v) => updateAuftragField('sache', v)}
                disabled={!canEdit}
              />
            </ObjTableRow>
            <ObjTableRow label="Wegen" herkunft={h('wegen')} onOpenInline={handleOpenInline}>
              <EditableField
                value={p.wegen}
                onSave={(v) => updateAuftragField('wegen', v)}
                disabled={!canEdit}
              />
            </ObjTableRow>
            <ObjTableRow label="Kostenvorschuss" herkunft={h('kostenvorschuss')} onOpenInline={handleOpenInline}>
              <EditableField
                value={p.kostenvorschuss}
                type="number"
                display={p.kostenvorschuss ? `€${p.kostenvorschuss.toLocaleString('de-DE')}` : null}
                onSave={(v) => updateAuftragField('kostenvorschuss', v)}
                disabled={!canEdit}
              />
            </ObjTableRow>
            <ObjTableRow label="Ausfertigungen (Druck)" herkunft={h('ausfertigungen')} onOpenInline={handleOpenInline}>
              <EditableField
                value={p.ausfertigungen}
                type="number"
                display={p.ausfertigungen ? `${p.ausfertigungen}×` : null}
                onSave={(v) => updateAuftragField('ausfertigungen', v)}
                disabled={!canEdit}
              />
            </ObjTableRow>
            <ObjTableRow label="Belastungen Abt. II" herkunft={h('belastungen_abt_2')} onOpenInline={handleOpenInline}>
              <EditableField
                value={p.belastungenAbt2}
                type="textarea"
                placeholder="— keine eingetragen —"
                onSave={(v) => updateAuftragField('belastungen_abt_2', v)}
                disabled={!canEdit}
              />
            </ObjTableRow>
          </StammdatenSektion>
      )}

      {/* Bearbeiter */}
      <StammdatenSektion title="Bearbeiter" fields={[p.sachverstaendiger, p.sachbearbeiter, p.notizen]} openSignal={feldOpen.key === 'sachverstaendiger' ? feldOpen.nonce : 0}>
          <ObjTableRow label="Sachverständiger" id="auftrag-feld-sachverstaendiger">
            {orgMembers && orgMembers.length > 0 ? (
              <EditableField
                value={p.sachverstaendiger || ''}
                openSignal={feldOpen.key === 'sachverstaendiger' ? feldOpen.nonce : 0}
                type="select"
                options={[{ value: '', label: '— nicht zugewiesen —' }, ...orgMembers.map(m => ({ value: m.name, label: m.name }))]}
                display={p.sachverstaendiger || '— nicht zugewiesen —'}
                onSave={(v) => updateAuftragField('sachverstaendiger', v || null)}
                disabled={!canEdit}
              />
            ) : (
              <EditableField value={p.sachverstaendiger} openSignal={feldOpen.key === 'sachverstaendiger' ? feldOpen.nonce : 0} onSave={(v) => updateAuftragField('sachverstaendiger', v)} disabled={!canEdit} placeholder="— nicht zugewiesen —" />
            )}
          </ObjTableRow>
          <ObjTableRow label="Sachbearbeiter">
            {orgMembers && orgMembers.length > 0 ? (
              <EditableField
                value={p.sachbearbeiter || ''}
                type="select"
                options={[{ value: '', label: '— nicht zugewiesen —' }, ...orgMembers.map(m => ({ value: m.name, label: m.name }))]}
                display={p.sachbearbeiter || '— nicht zugewiesen —'}
                onSave={(v) => updateAuftragField('sachbearbeiter', v || null)}
                disabled={!canEdit}
              />
            ) : (
              <EditableField value={p.sachbearbeiter} onSave={(v) => updateAuftragField('sachbearbeiter', v)} disabled={!canEdit} placeholder="— nicht zugewiesen —" />
            )}
          </ObjTableRow>
          <ObjTableRow label="Interne Notiz">
            <EditableField
              value={p.interneNotiz || ''}
              type="textarea"
              onSave={(v) => updateAuftragField('notizen', v || null)}
              disabled={!canEdit}
              placeholder="Interne Anmerkungen zum Auftrag (nur für Ihr Büro sichtbar)"
            />
          </ObjTableRow>
        </StammdatenSektion>

      {p.auftragsart_raw === 'gericht' && (
      <StammdatenSektion title="Nachgelagerte Schritte" fields={[p.zuschlag_datum, p.zuschlag_betrag, p.widerspruch_1_vom, p.frist_stellungnahme_1, p.stellungnahme_1_vom, p.anhoerung_1_vom]}>
          <ObjTableRow label="Zuschlag vom" sourceHint="Gerichtsprotokoll">
            <EditableField value={p.zuschlag_datum} type="date" onSave={(v) => updateAuftragField('zuschlag_datum', v)} disabled={!canEdit} />
          </ObjTableRow>
          <ObjTableRow label="Zuschlag zu (EUR)" sourceHint="Gerichtsprotokoll">
            <EditableField value={p.zuschlag_betrag} type="number" display={p.zuschlag_betrag ? `€${Number(p.zuschlag_betrag).toLocaleString('de-DE')}` : null} onSave={(v) => updateAuftragField('zuschlag_betrag', v)} disabled={!canEdit} />
          </ObjTableRow>
          <ObjTableRow label="Widerspruch 1 vom" sourceHint="Gerichtsschreiben">
            <EditableField value={p.widerspruch_1_vom} type="date" onSave={(v) => updateAuftragField('widerspruch_1_vom', v)} disabled={!canEdit} />
          </ObjTableRow>
          <ObjTableRow label="Frist Stellungnahme 1" sourceHint="Gerichtsschreiben">
            <EditableField value={p.frist_stellungnahme_1} type="date" onSave={(v) => updateAuftragField('frist_stellungnahme_1', v)} disabled={!canEdit} />
          </ObjTableRow>
          <ObjTableRow label="Stellungnahme 1 vom" sourceHint="eigenes Schreiben">
            <EditableField value={p.stellungnahme_1_vom} type="date" onSave={(v) => updateAuftragField('stellungnahme_1_vom', v)} disabled={!canEdit} />
          </ObjTableRow>
          <ObjTableRow label="Anhörung 1 vom" sourceHint="Gericht">
            <EditableField value={p.anhoerung_1_vom} type="date" onSave={(v) => updateAuftragField('anhoerung_1_vom', v)} disabled={!canEdit} />
          </ObjTableRow>
        </StammdatenSektion>
      )}
    </>
  );

  const beteiligtePanel = (
    <BeteiligtePanel
      beteiligte={p.beteiligte}
      herkunft={herkunft}
      projektId={p.id}
      session={session}
      workerUrl={workerUrl}
      onChanged={onRefresh}
      onOpenInline={handleOpenInline}
    />
  );

  // Layout-Modi:
  // A) Viewer aktiv: 3 Spalten wirken zu eng → 2 Spalten: (Daten + Beteiligte darunter) | Viewer
  // B) Viewer inaktiv: bewährtes 2-Spalten-Layout (Daten | Beteiligte)
  return (
    <div className="view-wrapper">
      <ZeitenKostenPanel
        p={p}
        updateAuftragField={updateAuftragField}
        session={session}
        workerUrl={workerUrl}
      />
      {viewerActive ? (
        <div style={{
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          gap: 'var(--space-5)',
          alignItems: 'start',
        }}>
          <div>
            {dataColumn}
            {beteiligtePanel}
          </div>
          <div style={{ position: 'sticky', top: 'var(--space-4)' }}>
            <DokumentViewer
              dokumente={auftragDokumente}
              session={session}
              workerUrl={workerUrl}
              activeId={viewerDokId}
              onChangeActive={setViewerDokId}
              placeholder="Keine Auftragsdokumente hochgeladen. Gerichtsbeschluss oder Anschreiben laden, um diese hier zu referenzieren."
            />
          </div>
        </div>
      ) : (
        <div className="auftrag-layout">
          <div className="reveal">{dataColumn}</div>
          {beteiligtePanel}
        </div>
      )}

      <AktivitaetenPanel
        projektId={p.id}
        beteiligte={p.beteiligte}
        session={session}
        workerUrl={workerUrl}
      />
    </div>
  );
};

const BeteiligtePanel = ({
  beteiligte, herkunft,
  projektId, session, workerUrl,
  onChanged,              // () => void — nach Insert/Update/Delete aufgerufen
  onOpenDocument, onOpenInline,
}) => {
  const [addOpen, setAddOpen] = useState(false);
  const [newRolle, setNewRolle] = useState('');
  const [newName, setNewName] = useState('');
  const [newAnschrift, setNewAnschrift] = useState('');
  const [newTelefon, setNewTelefon] = useState('');
  const [newEmail, setNewEmail] = useState('');
  const [newAktz, setNewAktz] = useState('');
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  const [deleteConfirm, setDeleteConfirm] = useState(null);

  const [addTouched, setAddTouched] = useState(false);
  const rolleInvalid = addTouched && newRolle.trim().length === 0;
  const nameInvalid = addTouched && newName.trim().length === 0;

  const canSave = newRolle.trim().length > 0 && newName.trim().length > 0;

  const resetForm = () => {
    setNewRolle(''); setNewName(''); setNewAnschrift(''); setNewTelefon(''); setNewEmail(''); setNewAktz('');
    setError(null); setAddTouched(false);
  };

  const handleStartAdd = (preselectedRolle) => {
    resetForm();
    if (preselectedRolle) setNewRolle(preselectedRolle);
    setAddOpen(true);
  };

  const handleSaveNew = async () => {
    if (!canSave) {
      setAddTouched(true);
      return;
    }
    setSaving(true); setError(null);
    try {
      await apiInsertRow('beteiligte', {
        project_id: projektId,
        rolle: newRolle.trim(),
        name: newName.trim(),
        anschrift: newAnschrift.trim() || null,
        telefon: newTelefon.trim() || null,
        email: newEmail.trim() || null,
        aktenzeichen: newAktz.trim() || null,
      }, session, workerUrl);
      resetForm();
      setAddOpen(false);
      onChanged && onChanged();
    } catch (err) {
      setError(err.message || String(err));
    } finally {
      setSaving(false);
    }
  };

  const handleUpdate = async (id, patch) => {
    await apiPatchRow('beteiligte', id, patch, session, workerUrl);
    onChanged && onChanged();
  };

  const handleDelete = async (b) => {
    setDeleteConfirm({ projekt: b, busy: false, error: null });
  };

  const executeDelete = async () => {
    if (!deleteConfirm) return;
    setDeleteConfirm(s => ({ ...s, busy: true, error: null }));
    try {
      await apiDeleteRow('beteiligte', deleteConfirm.projekt.id, session, workerUrl);
      setDeleteConfirm(null);
      onChanged && onChanged();
    } catch (err) {
      setDeleteConfirm(s => ({ ...s, busy: false, error: err.message || String(err) }));
    }
  };

  return (
    <div>
      <div className="card">
        <div className="card-header">
          <span className="card-title">Beteiligte ({beteiligte.length})</span>
        </div>
        <div className="beteiligte-list">
          {beteiligte.map((b, i) => {
            const h = herkunft?.[`beteiligte:${b.id}:_record`];
            return (
              <div key={b.id || i} className="beteiligte-item">
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 8 }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="beteiligte-rolle">
                      {session && b.id ? (
                        <EditableField
                          value={b.rolle}
                          onSave={async (v) => { if (v) await handleUpdate(b.id, { rolle: v }); }}
                          style={{ fontSize: 'inherit', fontWeight: 'inherit', letterSpacing: 'inherit', textTransform: 'inherit' }}
                        />
                      ) : b.rolle}
                    </div>
                    <div className="beteiligte-name">
                      {session && b.id ? (
                        <EditableField
                          value={b.name}
                          onSave={async (v) => { if (v) await handleUpdate(b.id, { name: v }); }}
                        />
                      ) : b.name}
                    </div>
                    {/* Detail-Felder einzeln editierbar, damit Anschrift,
                        Anwalt und Aktenzeichen nachträglich korrigiert
                        werden können. Ohne Session oder ID: Fallback auf
                        die vorformatierte detail-Zeile. */}
                    {session && b.id ? (
                      <div className="beteiligte-detail" style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
                        <div>
                          <EditableField
                            value={b.anschrift}
                            placeholder="— Anschrift —"
                            onSave={async (v) => handleUpdate(b.id, { anschrift: v })}
                          />
                        </div>
                        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-1, 4px)' }}>
                          <div>
                            <EditableField
                              value={b.telefon}
                              placeholder="— Telefon —"
                              onSave={async (v) => handleUpdate(b.id, { telefon: v })}
                            />
                          </div>
                          <div>
                            <EditableField
                              value={b.email}
                              placeholder="— E-Mail —"
                              onSave={async (v) => handleUpdate(b.id, { email: v })}
                            />
                          </div>
                        </div>
                        {(b.anwalt_name || b.anwalt_anschrift) && (
                          <div>
                            RA:{' '}
                            <EditableField
                              value={b.anwalt_name}
                              placeholder="— Anwalt —"
                              onSave={async (v) => handleUpdate(b.id, { anwalt_name: v })}
                            />
                          </div>
                        )}
                        <div>
                          Az.:{' '}
                          <EditableField
                            value={b.aktenzeichen}
                            placeholder="—"
                            onSave={async (v) => handleUpdate(b.id, { aktenzeichen: v })}
                          />
                        </div>
                      </div>
                    ) : (
                      b.detail && <div className="beteiligte-detail">{b.detail}</div>
                    )}
                  </div>
                  {/* Nur Delete-Button rechts oben — der Badge wandert in
                      eine eigene Zeile darunter, sonst zerquetscht er bei
                      schmalen Spalten den Text. */}
                  {session && b.id && (
                    <button
                      type="button"
                      onClick={() => handleDelete(b)}
                      title="Beteiligten entfernen"
                      style={{
                        background: 'none', border: 'none',
                        color: 'var(--text-tertiary)', cursor: 'pointer',
                        padding: '2px 6px', fontSize: 16, lineHeight: 1,
                        borderRadius: 'var(--radius-sm)',
                        flexShrink: 0,
                      }}
                      onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--danger)'; }}
                      onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--text-tertiary)'; }}
                    >×</button>
                  )}
                </div>
                {/* ProvenanceBadge in eigener Zeile am Ende des Items.
                    Volle Breite, links ausgerichtet, leichter Abstand
                    zum Detail-Block — das löst das Layout-Problem mit
                    schmalen Spalten und langen Badge-Texten. */}
                {h && (onOpenDocument || onOpenInline) && (
                  <div style={{ marginTop: 'var(--space-2)' }}>
                    <ProvenanceBadge
                      herkunft={h}
                      onOpen={onOpenDocument}
                      onOpenInline={onOpenInline}
                    />
                  </div>
                )}
              </div>
            );
          })}
          {beteiligte.length === 0 && (
            <div style={{ padding: 'var(--space-4)', color: 'var(--text-tertiary)', fontSize: 13 }}>
              Noch keine Beteiligten erfasst.
            </div>
          )}
        </div>

        <div className="beteiligte-add-menu">
          {!addOpen ? (
            <>
              <div className="beteiligte-add-title">+ Beteiligten hinzufügen</div>
              <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 'var(--space-3)' }}>
                Wähle eine Rolle (Vorschlag) oder gib eine eigene ein.
              </div>
              <div className="beteiligte-add-roles">
                {BETEILIGTE_ROLLEN_VORSCHLAEGE.map(rolle => (
                  <button
                    key={rolle}
                    type="button"
                    className="beteiligte-role-btn"
                    onClick={() => handleStartAdd(rolle)}
                  >{rolle}</button>
                ))}
                <button
                  type="button"
                  className="beteiligte-role-btn"
                  style={{ color: 'var(--vl-blue-light)' }}
                  onClick={() => handleStartAdd('')}
                >+ Eigene Rolle</button>
              </div>
            </>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
              <div className="beteiligte-add-title">Neuen Beteiligten anlegen</div>
              <div>
                <label style={formLabelStyle()}>Rolle *</label>
                <input
                  type="text" value={newRolle}
                  onChange={(e) => setNewRolle(e.target.value)}
                  placeholder="z.B. Rechtspfleger, Eigentümer, Mieter …"
                  style={{
                    ...formInputStyle(),
                    ...(rolleInvalid ? { borderColor: 'var(--danger)', boxShadow: '0 0 0 1px var(--danger)' } : {}),
                  }}
                  autoFocus
                />
                {rolleInvalid && (
                  <div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 2 }}>Rolle ist Pflicht.</div>
                )}
              </div>
              <div>
                <label style={formLabelStyle()}>Name *</label>
                <input
                  type="text" value={newName}
                  onChange={(e) => setNewName(e.target.value)}
                  placeholder="Vollständiger Name oder Firmenname"
                  style={{
                    ...formInputStyle(),
                    ...(nameInvalid ? { borderColor: 'var(--danger)', boxShadow: '0 0 0 1px var(--danger)' } : {}),
                  }}
                />
                {nameInvalid && (
                  <div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 2 }}>Name ist Pflicht.</div>
                )}
              </div>
              <div>
                <label style={formLabelStyle()}>Anschrift</label>
                <input
                  type="text" value={newAnschrift}
                  onChange={(e) => setNewAnschrift(e.target.value)}
                  placeholder="Straße, PLZ, Ort"
                  style={formInputStyle()}
                />
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-2)' }}>
                <div>
                  <label style={formLabelStyle()}>Telefon</label>
                  <input
                    type="tel" value={newTelefon}
                    onChange={(e) => setNewTelefon(e.target.value)}
                    placeholder="z.B. 0911 12345"
                    style={formInputStyle()}
                  />
                </div>
                <div>
                  <label style={formLabelStyle()}>E-Mail</label>
                  <input
                    type="email" value={newEmail}
                    onChange={(e) => setNewEmail(e.target.value)}
                    placeholder="name@beispiel.de"
                    style={formInputStyle()}
                  />
                </div>
              </div>
              <div>
                <label style={formLabelStyle()}>Aktenzeichen (optional)</label>
                <input
                  type="text" value={newAktz}
                  onChange={(e) => setNewAktz(e.target.value)}
                  style={formInputStyle()}
                />
              </div>
              {error && (
                <div style={{ color: 'var(--danger)', fontSize: 12 }}>{error}</div>
              )}
              <div style={{ display: 'flex', gap: 'var(--space-2)', justifyContent: 'flex-end' }}>
                <button
                  type="button"
                  className="btn btn-ghost btn-sm"
                  onClick={() => { setAddOpen(false); resetForm(); }}
                  disabled={saving}
                >Abbrechen</button>
                <button
                  type="button"
                  className="btn btn-primary btn-sm"
                  onClick={handleSaveNew}
                  disabled={saving}
                  style={!canSave && !saving ? { opacity: 0.55, cursor: 'default' } : {}}
                >{saving ? 'Speichert …' : 'Anlegen'}</button>
              </div>
            </div>
          )}
        </div>
      </div>

      {deleteConfirm && (
        <ConfirmDialog
          title="Beteiligten entfernen"
          message={<>
            <strong>{deleteConfirm.projekt.name}</strong> ({deleteConfirm.projekt.rolle}) wird aus der Liste entfernt.
            {deleteConfirm.error && <div style={{ color: 'var(--danger)', marginTop: 8 }}>{deleteConfirm.error}</div>}
          </>}
          confirmLabel="Entfernen"
          danger
          busy={deleteConfirm.busy}
          onConfirm={executeDelete}
          onCancel={() => setDeleteConfirm(null)}
        />
      )}
    </div>
  );
};

function formLabelStyle() {
  return {
    display: 'block', fontSize: 11, fontWeight: 600,
    textTransform: 'uppercase', letterSpacing: '0.08em',
    color: 'var(--text-secondary)', marginBottom: 4,
  };
}
function formInputStyle() {
  return {
    width: '100%', padding: '6px 10px', fontSize: 13,
    fontFamily: 'inherit', color: 'var(--text-primary)',
    background: 'var(--surface)',
    border: '1px solid var(--border-light)',
    borderRadius: 'var(--radius-sm)',
  };
}

// ══════════════════════════════════════════════════════════════════
// 4.3b · AKTIVITÄTEN-PANEL
// Timeline mit Notizen, Aufgaben, Terminen, Briefen.
// Lazy-Load: Daten werden erst beim Mounten geholt.
// ══════════════════════════════════════════════════════════════════

const AKTIVITAET_TYPEN = [
  { id: 'notiz',   label: 'Notiz',   icon: '✎', color: 'var(--vl-blue-light)' },
  { id: 'aufgabe', label: 'Aufgabe', icon: '☐', color: 'var(--warning)' },
  { id: 'termin',  label: 'Termin',  icon: '◷', color: 'var(--success)' },
  { id: 'brief',   label: 'Brief',   icon: '✉', color: 'var(--vl-orange)' },
  { id: 'system',  label: 'System',  icon: '⟳', color: 'var(--text-tertiary)' },
];

function aktivitaetTypMeta(typ) {
  return AKTIVITAET_TYPEN.find(t => t.id === typ) || AKTIVITAET_TYPEN[0];
}

// ────────────────────────────────────────────────────────────────────
// ActionButton — Primäraktion mit Bestätigungs-Moment (Apple-Pay-Gefühl):
// idle → laden (Spinner) → Erfolg (grüne Füllung + gezeichnetes Häkchen) → idle.
// onClick: async; wirft bei Fehler/ungültig → Button kehrt zurück (kein Erfolg).
// onDone: läuft nach der Erfolgs-Animation (z. B. Formular schließen).
// ────────────────────────────────────────────────────────────────────
const ActionButton = ({ onClick, onDone, children, className = 'btn btn-primary', style = {}, disabled = false, successDuration = 1100 }) => {
  const [state, setState] = useState('idle'); // 'idle' | 'loading' | 'success'
  const timerRef = useRef(null);
  useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);

  const handle = async () => {
    if (state !== 'idle' || disabled) return;
    setState('loading');
    try {
      await onClick?.();
    } catch {
      setState('idle');
      return;
    }
    setState('success');
    timerRef.current = setTimeout(() => {
      setState('idle');
      if (onDone) onDone();
    }, successDuration);
  };

  const isSuccess = state === 'success';
  const isLoading = state === 'loading';
  return (
    <button type="button" onClick={handle} disabled={disabled || isLoading || isSuccess}
      className={`${className} action-btn${isSuccess ? ' is-success' : ''}`} style={style}>
      <span className="action-btn-label" style={{ opacity: isSuccess ? 0 : 1 }}>
        {isLoading ? <span className="entwurf-spinner" aria-hidden="true" /> : children}
      </span>
      {isSuccess && (
        <span className="action-btn-check" aria-hidden="true">
          <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
            <path className="action-btn-check-path" d="M5 13l4 4L19 7" />
          </svg>
        </span>
      )}
    </button>
  );
};

// ────────────────────────────────────────────────────────────────────
// AnimatedCheckbox — abgerundete Checkbox, die sich beim Abhaken füllt
// und ein Häkchen zeichnet (ruhige Mikro-Interaktion fürs Erledigen).
// ────────────────────────────────────────────────────────────────────
const AnimatedCheckbox = ({ checked, onChange, size = 18, color = 'var(--vl-blue)', title }) => (
  <button type="button" role="checkbox" aria-checked={!!checked} title={title}
    onClick={(e) => { e.stopPropagation(); if (onChange) onChange(); }}
    className={`acheck${checked ? ' is-checked' : ''}`}
    style={{ '--acheck-size': `${size}px`, '--acheck-color': color }}>
    <svg viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path className="acheck-path" d="M5 13l4 4L19 7" />
    </svg>
  </button>
);

// ────────────────────────────────────────────────────────────────────
// Segmented — Apple-Segmentsteuerung mit gleitendem aktivem Feld.
// Der Indikator wird per Ref vermessen und gleitet zwischen den Optionen.
// ────────────────────────────────────────────────────────────────────
const Segmented = ({ options, value, onChange, ariaLabel, className = '' }) => {
  const ref = useRef(null);
  const [ind, setInd] = useState(null);
  React.useLayoutEffect(() => {
    const root = ref.current;
    if (!root) return;
    const el = root.querySelector(`[data-seg-val="${CSS.escape(String(value))}"]`);
    if (el) setInd({ left: el.offsetLeft, width: el.offsetWidth });
  }, [value, options]);
  useEffect(() => {
    const onResize = () => {
      const root = ref.current;
      if (!root) return;
      const el = root.querySelector(`[data-seg-val="${CSS.escape(String(value))}"]`);
      if (el) setInd({ left: el.offsetLeft, width: el.offsetWidth });
    };
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [value, options]);
  return (
    <div className={`seg seg-slider ${className}`} role="tablist" aria-label={ariaLabel} ref={ref}>
      {ind && <span className="seg-indicator" style={{ left: ind.left, width: ind.width }} aria-hidden="true" />}
      {options.map(o => (
        <button key={o.value} data-seg-val={o.value} type="button" role="tab"
          aria-selected={value === o.value}
          className={value === o.value ? 'is-active' : ''}
          onClick={() => onChange(o.value)}>{o.label}</button>
      ))}
    </div>
  );
};

const AktivitaetenPanel = ({ projektId, beteiligte, session, workerUrl }) => {
  const showToast = useToast();
  const orgMembers = useOrgMembers(session, workerUrl);
  const meId = (orgMembers || []).find(m => m.isMe)?.id || '';
  const memberName = (id) => (orgMembers || []).find(m => m.id === id)?.name || null;
  const [aktivitaeten, setAktivitaeten] = useState([]);
  const [loading, setLoading] = useState(true);
  const [loadError, setLoadError] = useState(null);
  const [filter, setFilter] = useState('alle');
  const [showAll, setShowAll] = useState(false);
  const [hideSystem, setHideSystem] = useState(true);
  const INITIAL_LIMIT = 5;

  // ── Create-Form State ──
  const [addOpen, setAddOpen] = useState(false);
  const [addTyp, setAddTyp] = useState('notiz');
  const [titel, setTitel] = useState('');
  const [inhalt, setInhalt] = useState('');
  const [bezugId, setBezugId] = useState('');
  const [faelligAm, setFaelligAm] = useState('');
  const [assigneeId, setAssigneeId] = useState('');
  const [terminDatum, setTerminDatum] = useState('');
  const [terminUhrzeit, setTerminUhrzeit] = useState('');
  const [terminOrt, setTerminOrt] = useState('');
  const [empfaenger, setEmpfaenger] = useState('');
  const [versandDatum, setVersandDatum] = useState('');
  const [anhang, setAnhang] = useState(null);
  const [saving, setSaving] = useState(false);
  const [formError, setFormError] = useState(null);
  const [formTouched, setFormTouched] = useState(false);
  const anhangRef = useRef(null);
  const [dragging, setDragging] = useState(false);

  const canSave = titel.trim().length >= 2;

  // ── Load ──
  const load = useCallback(async () => {
    if (!projektId || !session?.access_token) return;
    try {
      let token = session.access_token;
      try {
        const sb = await initSupabase();
        const { data } = await sb.auth.getSession();
        if (data?.session?.access_token) token = data.session.access_token;
      } catch {}
      const res = await fetch(
        `${workerUrl}/api/aktivitaeten?project_id=${encodeURIComponent(projektId)}`,
        { headers: { Authorization: `Bearer ${token}` } }
      );
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      setAktivitaeten(data.aktivitaeten || []);
      setLoadError(null);
    } catch (err) {
      setLoadError(err.message);
    } finally {
      setLoading(false);
    }
  }, [projektId, session, workerUrl]);

  useEffect(() => { load(); }, [load]);

  // ── Reset Form ──
  const resetForm = () => {
    setTitel(''); setInhalt(''); setBezugId('');
    setFaelligAm(''); setTerminDatum(''); setTerminUhrzeit('');
    setTerminOrt(''); setEmpfaenger(''); setVersandDatum('');
    setAnhang(null); setFormError(null); setFormTouched(false); setDragging(false);
    setAssigneeId('');
    if (anhangRef.current) anhangRef.current.value = '';
  };

  const openAdd = (typ) => {
    resetForm();
    setAddTyp(typ);
    setAssigneeId(typ === 'aufgabe' ? meId : '');
    setAddOpen(true);
  };

  // ── Create ──
  const handleCreate = async () => {
    if (!canSave) { setFormTouched(true); throw new Error('invalid'); }
    setSaving(true); setFormError(null);
    try {
      const row = {
        project_id: projektId,
        typ: addTyp,
        titel: titel.trim(),
        inhalt: inhalt.trim() || null,
        bezug_beteiligter_id: bezugId || null,
        faellig_am: addTyp === 'aufgabe' && faelligAm ? faelligAm : null,
        zugewiesen_an: addTyp === 'aufgabe' ? (assigneeId || null) : null,
        termin_datum: addTyp === 'termin' && terminDatum ? terminDatum : null,
        termin_uhrzeit: addTyp === 'termin' && terminUhrzeit ? terminUhrzeit : null,
        termin_ort: addTyp === 'termin' && terminOrt.trim() ? terminOrt.trim() : null,
        empfaenger: addTyp === 'brief' && empfaenger.trim() ? empfaenger.trim() : null,
        versand_datum: addTyp === 'brief' && versandDatum ? versandDatum : null,
      };

      const insertRes = await apiInsertRow('aktivitaeten', row, session, workerUrl);
      const newId = insertRes?.row?.id;

      // Anhang hochladen wenn vorhanden
      if (anhang && newId) {
        const fd = new FormData();
        fd.append('file', anhang);
        fd.append('project_id', projektId);
        fd.append('aktivitaet_id', newId);
        await fetch(`${workerUrl}/api/aktivitaet-upload`, {
          method: 'POST',
          headers: { Authorization: `Bearer ${session.access_token}` },
          body: fd,
        });
      }

      load();
      showToast(`${aktivitaetTypMeta(addTyp).label} gespeichert`, 'success');
    } catch (err) {
      setFormError(err.message || String(err));
      throw err;
    } finally {
      setSaving(false);
    }
  };

  // ── Toggle Erledigt ──
  const toggleErledigt = async (a) => {
    const newVal = !a.erledigt;
    try {
      await apiPatchRow('aktivitaeten', a.id, {
        erledigt: newVal,
        erledigt_am: newVal ? new Date().toISOString() : null,
      }, session, workerUrl);
      setAktivitaeten(prev => prev.map(x =>
        x.id === a.id ? { ...x, erledigt: newVal, erledigt_am: newVal ? new Date().toISOString() : null } : x
      ));
    } catch {}
  };

  // ── Pin Toggle ──
  const togglePin = async (a) => {
    try {
      await apiPatchRow('aktivitaeten', a.id, { angeheftet: !a.angeheftet }, session, workerUrl);
      load();
    } catch {}
  };

  // ── Aufgabe zuweisen / abnehmen ──
  const reassign = async (a, value) => {
    const v = value || null;
    setAktivitaeten(prev => prev.map(x => x.id === a.id ? { ...x, zugewiesen_an: v } : x));
    try {
      await apiPatchRow('aktivitaeten', a.id, { zugewiesen_an: v }, session, workerUrl);
    } catch { load(); }
  };

  // ── Delete ──
  const [deleteId, setDeleteId] = useState(null);
  const handleDelete = async () => {
    if (!deleteId) return;
    try {
      await apiDeleteRow('aktivitaeten', deleteId, session, workerUrl);
      setDeleteId(null);
      load();
      showToast('Aktivität gelöscht', 'success');
    } catch {}
  };

  // ── Filter ──
  const filtered = filter === 'alle'
    ? aktivitaeten.filter(a => (hideSystem ? a.typ !== 'system' : true))
    : aktivitaeten.filter(a => a.typ === filter);
  const systemCount = aktivitaeten.filter(a => a.typ === 'system').length;

  const offeneAufgaben = aktivitaeten.filter(a => a.typ === 'aufgabe' && !a.erledigt).length;

  // ── Render helpers ──
  const fmtDate = (d) => {
    if (!d) return '';
    try { return new Date(d).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); }
    catch { return d; }
  };
  const fmtDateTime = (d) => {
    if (!d) return '';
    try { return new Date(d).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); }
    catch { return d; }
  };
  const fmtTime = (t) => t ? t.substring(0, 5) : '';

  const beteiligterName = (id) => {
    const b = (beteiligte || []).find(x => x.id === id);
    return b ? `${b.rolle}: ${b.name}` : null;
  };

  // ── Form ──
  const titelInvalid = formTouched && titel.trim().length < 2;
  const renderCreateForm = () => (
    <div style={{
      padding: 'var(--space-4)',
      borderBottom: '1px solid var(--border-light)',
      background: 'var(--surface-light)',
    }}>
      <div style={{ display: 'flex', gap: 6, marginBottom: 'var(--space-3)', flexWrap: 'wrap' }}>
        {AKTIVITAET_TYPEN.map(t => (
          <button key={t.id} type="button"
            onClick={() => { setAddTyp(t.id); if (t.id === 'aufgabe') setAssigneeId(meId); }}
            style={{
              padding: '4px 12px', fontSize: 12, fontWeight: 600,
              border: `1px solid ${addTyp === t.id ? t.color : 'var(--border-light)'}`,
              background: addTyp === t.id ? `${t.color}18` : 'var(--surface)',
              color: addTyp === t.id ? t.color : 'var(--text-secondary)',
              borderRadius: 'var(--radius-sm)', cursor: 'pointer',
            }}
          >{t.icon} {t.label}</button>
        ))}
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
        <div>
          <label style={formLabelStyle()}>Titel *</label>
          <input type="text" value={titel} onChange={e => setTitel(e.target.value)}
            placeholder={addTyp === 'notiz' ? 'z.B. Telefonat mit RA Danowski' : addTyp === 'aufgabe' ? 'z.B. Gutachten einreichen' : addTyp === 'termin' ? 'z.B. Besichtigung Wassertrüdingen' : 'z.B. Bestätigung an Amtsgericht'}
            style={{ ...formInputStyle(), ...(titelInvalid ? { borderColor: 'var(--danger)' } : {}) }}
            autoFocus
          />
          {titelInvalid && <div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 2 }}>Titel ist Pflicht (mind. 2 Zeichen).</div>}
        </div>

        <div>
          <label style={formLabelStyle()}>Inhalt</label>
          <textarea value={inhalt} onChange={e => setInhalt(e.target.value)}
            placeholder="Freitext, Gesprächsnotiz, Beschreibung ..."
            rows={3}
            style={{ ...formInputStyle(), resize: 'vertical' }}
          />
        </div>

        {/* Typ-spezifische Felder */}
        {addTyp === 'aufgabe' && (
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-2)' }}>
            <div>
              <label style={formLabelStyle()}>Fällig am</label>
              <input type="date" value={faelligAm} onChange={e => setFaelligAm(e.target.value)} style={formInputStyle()} />
            </div>
            <div>
              <label style={formLabelStyle()}>Zugewiesen an</label>
              <select value={assigneeId} onChange={e => setAssigneeId(e.target.value)} style={formInputStyle()}>
                <option value="">— nicht zugewiesen —</option>
                {(orgMembers || []).map(m => <option key={m.id} value={m.id}>{m.name}{m.isMe ? ' (ich)' : ''}</option>)}
              </select>
            </div>
          </div>
        )}

        {addTyp === 'termin' && (
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-2)' }}>
            <div>
              <label style={formLabelStyle()}>Datum</label>
              <input type="date" value={terminDatum} onChange={e => setTerminDatum(e.target.value)} style={formInputStyle()} />
            </div>
            <div>
              <label style={formLabelStyle()}>Uhrzeit</label>
              <input type="time" value={terminUhrzeit} onChange={e => setTerminUhrzeit(e.target.value)} style={formInputStyle()} />
            </div>
            <div style={{ gridColumn: '1 / -1' }}>
              <label style={formLabelStyle()}>Ort</label>
              <input type="text" value={terminOrt} onChange={e => setTerminOrt(e.target.value)} placeholder="Adresse oder Ort" style={formInputStyle()} />
            </div>
          </div>
        )}

        {addTyp === 'brief' && (
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-2)' }}>
            <div>
              <label style={formLabelStyle()}>Empfänger</label>
              <input type="text" value={empfaenger} onChange={e => setEmpfaenger(e.target.value)} placeholder="Name oder Institution" style={formInputStyle()} />
            </div>
            <div>
              <label style={formLabelStyle()}>Versanddatum</label>
              <input type="date" value={versandDatum} onChange={e => setVersandDatum(e.target.value)} style={formInputStyle()} />
            </div>
          </div>
        )}

        {/* Bezug + Anhang */}
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-2)' }}>
          <div>
            <label style={formLabelStyle()}>Bezug (Beteiligter)</label>
            <select value={bezugId} onChange={e => setBezugId(e.target.value)} style={formInputStyle()}>
              <option value="">-- kein Bezug --</option>
              {(beteiligte || []).map(b => (
                <option key={b.id} value={b.id}>{b.rolle}: {b.name}</option>
              ))}
            </select>
          </div>
          <div>
            <label style={formLabelStyle()}>Anhang (PDF, E-Mail ...)</label>
            <input ref={anhangRef} type="file" style={{ display: 'none' }}
              onChange={e => { setAnhang(e.target.files?.[0] || null); setDragging(false); }} />
            {anhang ? (
              <div style={{
                padding: '8px 12px', borderRadius: 'var(--radius-sm)',
                border: '1px solid var(--success)', background: 'var(--success-bg)',
                display: 'flex', alignItems: 'center', gap: 8, fontSize: 12,
              }}>
                <span style={{ flex: 1, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {anhang.name}
                </span>
                <span style={{ color: 'var(--text-tertiary)', flexShrink: 0 }}>
                  {(anhang.size / 1024).toFixed(0)} KB
                </span>
                <button type="button" onClick={() => { setAnhang(null); if (anhangRef.current) anhangRef.current.value = ''; }}
                  style={{ background: 'none', border: 'none', color: 'var(--danger)', cursor: 'pointer', fontSize: 12, padding: '0 4px' }}
                >Entfernen</button>
              </div>
            ) : (
              <div
                onClick={() => anhangRef.current?.click()}
                onDragOver={e => { e.preventDefault(); e.stopPropagation(); setDragging(true); }}
                onDragEnter={e => { e.preventDefault(); e.stopPropagation(); setDragging(true); }}
                onDragLeave={e => { e.preventDefault(); e.stopPropagation(); setDragging(false); }}
                onDrop={e => {
                  e.preventDefault(); e.stopPropagation(); setDragging(false);
                  const f = e.dataTransfer?.files?.[0];
                  if (f) setAnhang(f);
                }}
                style={{
                  padding: '14px 12px',
                  border: `2px dashed ${dragging ? 'var(--vl-blue)' : 'var(--border-light)'}`,
                  borderRadius: 'var(--radius-sm)',
                  background: dragging ? 'var(--surface-blue, #EEF3F8)' : 'var(--surface-light)',
                  cursor: 'pointer', textAlign: 'center',
                  transition: 'border-color 0.15s, background 0.15s',
                  fontSize: 12, color: 'var(--text-secondary)',
                }}
              >
                <div style={{ fontWeight: 600, marginBottom: 2 }}>
                  {dragging ? 'Loslassen zum Anhängen' : 'Datei hierher ziehen oder klicken'}
                </div>
                <div style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                  PDF, E-Mail, Bild (max. 20 MB)
                </div>
              </div>
            )}
          </div>
        </div>

        {formError && <div style={{ color: 'var(--danger)', fontSize: 12 }}>{formError}</div>}

        <div style={{ display: 'flex', gap: 'var(--space-2)', justifyContent: 'flex-end', marginTop: 'var(--space-2)' }}>
          <button type="button" className="btn btn-ghost btn-sm" onClick={() => { setAddOpen(false); resetForm(); }} disabled={saving}>Abbrechen</button>
          <ActionButton onClick={handleCreate} onDone={() => { resetForm(); setAddOpen(false); }}
            className="btn btn-primary btn-sm" style={!canSave && !saving ? { opacity: 0.55 } : {}}
          >Speichern</ActionButton>
        </div>
      </div>
    </div>
  );

  // ── Timeline Entry ──
  const renderEntry = (a) => {
    const meta = aktivitaetTypMeta(a.typ);
    const isOverdue = a.typ === 'aufgabe' && !a.erledigt && a.faellig_am && new Date(a.faellig_am) < new Date();
    const bezugName = a.bezug_beteiligter_id ? beteiligterName(a.bezug_beteiligter_id) : null;
    const isSystem = a.typ === 'system';
    // Maschinelle Zähl-Details bei System-Aktivitäten ("13 Felder übernommen, 1 Beteiligte",
    // "5 von 138 Abschnitten erzeugt") helfen dem Nutzer nicht → ausblenden.
    // Echte System-Infos (z.B. "Frist: …") bleiben sichtbar.
    const istZaehlerDetail = isSystem && !!a.inhalt &&
      /Felder übernommen|\d+\s+Beteiligte|Abschnitten erzeugt/.test(a.inhalt);

    return (
      <div key={a.id} style={{
        padding: isSystem ? 'var(--space-2) var(--space-4)' : 'var(--space-3) var(--space-4)',
        borderBottom: '1px solid var(--border-light)',
        display: 'flex', gap: 'var(--space-3)',
        opacity: a.erledigt ? 0.6 : isSystem ? 0.7 : 1,
        background: a.angeheftet ? 'var(--surface-blue, #EEF3F8)' : 'transparent',
      }}>
        {/* Type indicator / Aufgaben-Checkbox */}
        {a.typ === 'aufgabe' ? (
          <div style={{ marginTop: 2, flexShrink: 0 }}>
            <AnimatedCheckbox checked={!!a.erledigt} onChange={() => toggleErledigt(a)} size={20}
              title={a.erledigt ? 'Als offen markieren' : 'Als erledigt markieren'} />
          </div>
        ) : (
          <div style={{
            width: isSystem ? 22 : 28, height: isSystem ? 22 : 28, borderRadius: 'var(--radius-sm)',
            background: `${meta.color}18`, color: meta.color,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: isSystem ? 11 : 14, fontWeight: 600, flexShrink: 0, marginTop: 2,
          }}>
            {meta.icon}
          </div>
        )}

        {/* Content */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
            <span style={{
              fontSize: isSystem ? 12 : 14, fontWeight: isSystem ? 500 : 600,
              textDecoration: a.erledigt ? 'line-through' : 'none',
              color: isSystem ? 'var(--text-secondary)' : undefined,
            }}>{a.titel}</span>
            <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
              {fmtDateTime(a.erstellt_am)}
            </span>
            {a.angeheftet && <span style={{ fontSize: 10, color: meta.color }}>angeheftet</span>}
          </div>

          {a.inhalt && !istZaehlerDetail && (
            <div style={{
              fontSize: 13, color: 'var(--text-secondary)', marginTop: 2,
              whiteSpace: 'pre-wrap', lineHeight: 1.5,
            }}>{a.inhalt}</div>
          )}

          {/* Typ-spezifische Details */}
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 4 }}>
            {a.typ === 'aufgabe' && a.faellig_am && (
              <span style={{
                fontSize: 11, fontWeight: 600,
                padding: '1px 6px', borderRadius: 'var(--radius-sm)',
                background: isOverdue ? 'var(--danger-bg)' : 'var(--warning-bg, #FFF8E1)',
                color: isOverdue ? 'var(--danger)' : 'var(--warning)',
              }}>
                Fällig: {fmtDate(a.faellig_am)}
              </span>
            )}
            {a.typ === 'aufgabe' && (
              <label onClick={e => e.stopPropagation()} title="Zugewiesen an" style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
                <select value={a.zugewiesen_an || ''} onChange={e => reassign(a, e.target.value)}
                  style={{ fontSize: 11, border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)', padding: '1px 4px', background: 'var(--surface)', color: a.zugewiesen_an ? 'var(--text-secondary)' : 'var(--text-tertiary)', maxWidth: 150, cursor: 'pointer' }}>
                  <option value="">nicht zugewiesen</option>
                  {(orgMembers || []).map(m => <option key={m.id} value={m.id}>{m.name}{m.isMe ? ' (ich)' : ''}</option>)}
                </select>
              </label>
            )}
            {a.typ === 'termin' && a.termin_datum && (
              <span style={{
                fontSize: 11, fontWeight: 600, padding: '1px 6px',
                borderRadius: 'var(--radius-sm)', background: 'var(--success-bg)', color: 'var(--success)',
              }}>
                {fmtDate(a.termin_datum)}{a.termin_uhrzeit ? `, ${fmtTime(a.termin_uhrzeit)}` : ''}{a.termin_ort ? ` — ${a.termin_ort}` : ''}
              </span>
            )}
            {a.typ === 'brief' && a.empfaenger && (
              <span style={{
                fontSize: 11, fontWeight: 600, padding: '1px 6px',
                borderRadius: 'var(--radius-sm)', background: 'var(--vl-orange-bg)', color: 'var(--vl-orange-dark)',
              }}>
                An: {a.empfaenger}{a.versand_datum ? ` — ${fmtDate(a.versand_datum)}` : ''}
              </span>
            )}
            {bezugName && (
              <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                → {bezugName}
              </span>
            )}
            {a.file_name && (
              <div style={{
                display: 'flex', alignItems: 'center', gap: 6,
                marginTop: 4, padding: '4px 8px',
                background: 'var(--surface-light)', borderRadius: 4,
                fontSize: 11, color: 'var(--text-secondary)',
              }}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
                  <polyline points="14 2 14 8 20 8"/>
                </svg>
                <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {a.file_name}
                </span>
                <button type="button" onClick={async (e) => {
                  e.stopPropagation();
                  try {
                    const res = await fetch(
                      `${workerUrl}/api/signed-url?bucket=documents&path=${encodeURIComponent(a.file_path)}`,
                      { headers: { Authorization: `Bearer ${session.access_token}` } }
                    );
                    if (res.ok) {
                      const { url } = await res.json();
                      window.open(url, '_blank');
                    }
                  } catch {}
                }} style={{
                  background: 'none', border: 'none', cursor: 'pointer',
                  color: 'var(--vl-blue)', fontSize: 11, fontWeight: 600,
                  padding: '2px 4px', flexShrink: 0,
                }}>
                  Öffnen
                </button>
                <button type="button" onClick={async (e) => {
                  e.stopPropagation();
                  try {
                    const res = await fetch(
                      `${workerUrl}/api/signed-url?bucket=documents&path=${encodeURIComponent(a.file_path)}`,
                      { headers: { Authorization: `Bearer ${session.access_token}` } }
                    );
                    if (res.ok) {
                      const { url } = await res.json();
                      const link = document.createElement('a');
                      link.href = url;
                      link.download = a.file_name;
                      link.click();
                    }
                  } catch {}
                }} style={{
                  background: 'none', border: 'none', cursor: 'pointer',
                  color: 'var(--text-tertiary)', fontSize: 11,
                  padding: '2px 4px', flexShrink: 0,
                }}>
                  ↓
                </button>
              </div>
            )}
          </div>
        </div>

        {/* Actions — nicht für System-Einträge */}
        {!isSystem && (
        <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
          <button type="button" onClick={() => togglePin(a)}
            title={a.angeheftet ? 'Lösen' : 'Anheften'}
            style={{
              background: 'none', border: 'none', cursor: 'pointer',
              fontSize: 12, color: a.angeheftet ? meta.color : 'var(--text-tertiary)',
              padding: '2px 4px',
            }}
          >📌</button>
          <button type="button" onClick={() => setDeleteId(a.id)}
            title="Löschen"
            style={{
              background: 'none', border: 'none', cursor: 'pointer',
              fontSize: 12, color: 'var(--text-tertiary)', padding: '2px 4px',
            }}
            onMouseEnter={e => { e.currentTarget.style.color = 'var(--danger)'; }}
            onMouseLeave={e => { e.currentTarget.style.color = 'var(--text-tertiary)'; }}
          >×</button>
        </div>
        )}
      </div>
    );
  };

  // ── Main Render ──
  return (
    <div className="card" style={{ marginTop: 'var(--space-5)' }}>
      <div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span className="card-title">Aktivitäten ({aktivitaeten.length})</span>
          {offeneAufgaben > 0 && (
            <span style={{
              fontSize: 11, fontWeight: 600, padding: '1px 6px',
              borderRadius: 'var(--radius-sm)',
              background: 'var(--warning-bg, #FFF8E1)', color: 'var(--warning)',
            }}>{offeneAufgaben} offen</span>
          )}
        </div>
        {!addOpen && (
          <button type="button" className="btn btn-primary btn-sm"
            onClick={() => openAdd('notiz')}
            style={{ fontSize: 12, display: 'inline-flex', alignItems: 'center', gap: 6 }}>
            <span style={{ fontSize: 15, lineHeight: 1, marginTop: -1 }}>+</span> Neu
          </button>
        )}
      </div>

      {addOpen && renderCreateForm()}

      {/* Filter */}
      {aktivitaeten.length > 0 && (
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          gap: 8, padding: 'var(--space-2) var(--space-4)',
          borderBottom: '1px solid var(--border-light)', flexWrap: 'wrap',
        }}>
          <Segmented ariaLabel="Aktivitäten filtern"
            value={filter}
            onChange={(v) => { setFilter(v); setShowAll(false); }}
            options={[{ value: 'alle', label: 'Alle' }, { value: 'aufgabe', label: 'Aufgaben' }, { value: 'notiz', label: 'Notizen' }, { value: 'termin', label: 'Termine' }, { value: 'brief', label: 'Briefe' }]}
          />
          {filter === 'alle' && systemCount > 0 && (
            <button type="button" className="btn btn-ghost btn-sm"
              onClick={() => setHideSystem(h => !h)}
              style={{ fontSize: 11, color: 'var(--text-tertiary)', whiteSpace: 'nowrap' }}>
              {hideSystem ? `⟳ ${systemCount} Systemeinträge einblenden` : 'Systemeinträge ausblenden'}
            </button>
          )}
        </div>
      )}

      {/* Timeline */}
      {loading ? (
        <div style={{ padding: 'var(--space-5)', textAlign: 'center', color: 'var(--text-tertiary)' }}>Lade Aktivitäten ...</div>
      ) : loadError ? (
        <div style={{ padding: 'var(--space-4)', color: 'var(--danger)', fontSize: 13 }}>{loadError}</div>
      ) : filtered.length === 0 ? (
        <div style={{ padding: 'var(--space-5)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>
          {aktivitaeten.length === 0
            ? 'Noch keine Aktivitäten. Lege eine Notiz, Aufgabe oder einen Termin an.'
            : (filter === 'alle' && hideSystem && systemCount > 0)
              ? <>Nur Systemeinträge vorhanden. <button type="button" onClick={() => setHideSystem(false)} style={{ background: 'none', border: 'none', color: 'var(--vl-blue)', cursor: 'pointer', fontWeight: 600, padding: 0 }}>{systemCount} einblenden</button></>
              : 'Keine Einträge für diesen Filter.'}
        </div>
      ) : (() => {
        const visible = showAll ? filtered : filtered.slice(0, INITIAL_LIMIT);
        const hidden = filtered.length - INITIAL_LIMIT;
        return (
          <>
            {visible.map(a => renderEntry(a))}
            {!showAll && hidden > 0 && (
              <div style={{ padding: '12px 16px', textAlign: 'center' }}>
                <button className="btn btn-ghost btn-sm" style={{ fontSize: 12, color: 'var(--text-secondary)' }}
                  onClick={() => setShowAll(true)}>
                  {hidden} weitere anzeigen
                </button>
              </div>
            )}
            {showAll && filtered.length > INITIAL_LIMIT && (
              <div style={{ padding: '12px 16px', textAlign: 'center' }}>
                <button className="btn btn-ghost btn-sm" style={{ fontSize: 12, color: 'var(--text-secondary)' }}
                  onClick={() => setShowAll(false)}>
                  Weniger anzeigen
                </button>
              </div>
            )}
          </>
        );
      })()}

      {/* Delete Confirm */}
      {deleteId && (
        <ConfirmDialog
          title="Aktivität löschen"
          message="Diese Aktivität wird unwiderruflich entfernt."
          confirmLabel="Löschen"
          danger
          onConfirm={handleDelete}
          onCancel={() => setDeleteId(null)}
        />
      )}
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════
// 4.4 · VIEW: GUTACHTEN
// ══════════════════════════════════════════════════════════════════

const GutachtenSwitcher = ({ p, aktiverIdx, onSwitch, session, workerUrl, onRefresh }) => {
  const [open, setOpen] = useState(false);
  const switcherRef = useRef(null);

  useEffect(() => {
    const onClickOutside = (e) => {
      if (switcherRef.current && !switcherRef.current.contains(e.target)) {
        setOpen(false);
      }
    };
    document.addEventListener('click', onClickOutside);
    return () => document.removeEventListener('click', onClickOutside);
  }, []);

  return (
    <div className="gutachten-switcher" ref={switcherRef}>
      <button
        className="gutachten-switcher-button"
        onClick={(e) => { e.stopPropagation(); setOpen(!open); }}
      >
        Gutachten {aktiverIdx + 1} von {p.gutachten.length}
        <IconChevronDown size={16} />
      </button>
      {open && (
        <div className="gutachten-switcher-dropdown">
          {p.gutachten.map((gg, i) => (
            <div
              key={i}
              className={`gutachten-switcher-item ${i === aktiverIdx ? 'current' : ''}`}
              onClick={() => { onSwitch(i); setOpen(false); }}
            >
              <div className="gutachten-switcher-item-title">
                Gutachten {i + 1} — {gg.titel.split('—')[0].trim()}
              </div>
              <div className="gutachten-switcher-item-sub">{gg.adresse}</div>
            </div>
          ))}
          <div
            className="gutachten-switcher-item gutachten-switcher-item-add"
            onClick={async () => {
              setOpen(false);
              try {
                const sortOrder = p.gutachten.length;
                const created = await apiInsertRow('gutachten', {
                  project_id: p.id,
                  titel: `Gutachten ${sortOrder + 1}`,
                  sort_order: sortOrder,
                }, session, workerUrl);
                if (created?.row?.id) {
                  await apiInsertRow('bewertungsobjekte', {
                    gutachten_id: created.row.id,
                    bezeichnung: 'Objekt 1',
                    sort_order: 0,
                  }, session, workerUrl);
                }
                if (onRefresh) onRefresh();
              } catch (e) { alert('Fehler: ' + (e.message || e)); }
            }}
          >
            + Weiteres Gutachten anlegen
          </div>
        </div>
      )}
    </div>
  );
};

const GutachtenToolbar = ({ p, g, aktiverIdx, gutachtenTab, onSwitchGutachten, onOpenAuftrag, onDeleteGutachten, session, workerUrl, onRefresh }) => {
  const multiG = p.gutachten.length > 1;
  const isMobile = useIsMobile();
  const [imgUrl, setImgUrl] = useState(null);
  const [uploading, setUploading] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const fileRef = useRef(null);
  const menuRef = useRef(null);

  // Close menu on outside click
  useEffect(() => {
    if (!menuOpen) return;
    const handleClick = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); };
    document.addEventListener('mousedown', handleClick);
    return () => document.removeEventListener('mousedown', handleClick);
  }, [menuOpen]);

  useEffect(() => {
    if (!g.titelbild_path || !session?.access_token || !workerUrl) return;
    let cancelled = false;
    fetch(`${workerUrl}/api/signed-url?path=${encodeURIComponent(g.titelbild_path)}`, {
      headers: { Authorization: `Bearer ${session.access_token}` },
    })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (!cancelled && d?.url) setImgUrl(d.url); })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [g.titelbild_path, session, workerUrl]);

  const handleUpload = async (e) => {
    const file = e.target?.files?.[0];
    if (!file || !session?.access_token) return;
    setUploading(true);
    try {
      const blob = await new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
          const maxW = 800;
          const scale = img.width > maxW ? maxW / img.width : 1;
          const c = document.createElement('canvas');
          c.width = img.width * scale; c.height = img.height * scale;
          c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
          c.toBlob(b => resolve(b), 'image/jpeg', 0.85);
        };
        img.src = URL.createObjectURL(file);
      });
      const fd = new FormData();
      fd.append('file', blob, 'titelbild.jpg');
      fd.append('project_id', p.id);
      fd.append('gutachten_id', g.id);
      const upRes = await fetch(`${workerUrl}/api/photo-upload`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${session.access_token}` },
        body: fd,
      });
      if (!upRes.ok) throw new Error('Upload fehlgeschlagen');
      const { storage_path } = await upRes.json();
      await apiPatchRow('gutachten', g.id, { titelbild_path: storage_path }, session, workerUrl);
      if (onRefresh) onRefresh();
    } catch (err) {
      console.error('Titelbild-Upload:', err);
    } finally {
      setUploading(false);
      if (fileRef.current) fileRef.current.value = '';
    }
  };

  return (
    <div className="gutachten-toolbar" style={{ display: 'flex', alignItems: isMobile ? 'center' : 'flex-start', gap: isMobile ? 10 : 'var(--space-4)' }}>
      {/* Titelbild aus der Kopfleiste entfernt — das Objektfoto lebt jetzt im Auftrags-Steckbrief. */}

      <div style={{ flex: 1, minWidth: 0 }}>
        <EditableField
          value={g.titel}
          placeholder="Gutachten-Titel"
          onSave={async (v) => {
            await apiPatchRow('gutachten', g.id, { titel: v }, session, workerUrl);
            if (onRefresh) onRefresh();
          }}
          style={{ fontSize: isMobile ? 15 : 18, fontWeight: 700, lineHeight: 1.3 }}
        />
        {g.adresse && (
          <EditableField
            value={g.adresse}
            placeholder="Adresse"
            onSave={async (v) => {
              await apiPatchRow('gutachten', g.id, { adresse: v }, session, workerUrl);
              if (onRefresh) onRefresh();
            }}
            style={{ fontSize: isMobile ? 12 : 13, color: 'var(--text-secondary)', marginTop: 2 }}
          />
        )}
      </div>
      {multiG && <GutachtenSwitcher p={p} aktiverIdx={aktiverIdx} onSwitch={onSwitchGutachten} session={session} workerUrl={workerUrl} onRefresh={onRefresh} />}
      <button className="btn btn-ghost" onClick={onOpenAuftrag}
        style={isMobile ? { padding: '6px 10px', fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 } : undefined}>
        Auftrag →
      </button>

      {/* Mehr-Menü */}
      <div ref={menuRef} style={{ position: 'relative' }}>
        <button className="btn btn-ghost" onClick={() => setMenuOpen(!menuOpen)}
          style={{ padding: '6px 8px', fontSize: 16, lineHeight: 1 }}>⋯</button>
        {menuOpen && (
          <div style={{
            position: 'absolute', top: '100%', right: 0, zIndex: 100,
            background: '#fff', border: '1px solid var(--border-light)',
            borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)',
            minWidth: 180, padding: 4,
          }}>
            <button
              onClick={() => { setMenuOpen(false); setConfirmDelete(true); }}
              style={{
                width: '100%', textAlign: 'left', padding: '8px 12px',
                background: 'none', border: 'none', cursor: 'pointer',
                fontSize: 13, color: 'var(--danger)', borderRadius: 'var(--radius-sm)',
              }}
              onMouseEnter={e => { e.currentTarget.style.background = 'rgba(220,38,38,0.06)'; }}
              onMouseLeave={e => { e.currentTarget.style.background = 'none'; }}
            >Gutachten löschen …</button>
          </div>
        )}
      </div>

      {/* Lösch-Bestätigung */}
      {confirmDelete && (
        <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
          onClick={() => setConfirmDelete(false)}>
          <div className="card" style={{ maxWidth: 420, padding: 24 }} onClick={e => e.stopPropagation()}>
            <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 8, color: 'var(--danger)' }}>Gutachten löschen</div>
            <p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16, lineHeight: 1.5 }}>
              <strong>{g.titel || 'Dieses Gutachten'}</strong> wird unwiderruflich gelöscht, inklusive aller Bewertungsobjekte, Entwurfs-Texte und zugeordneten Dokumente.
              {p.gutachten.length <= 1 && <><br /><br />Da dies das einzige Gutachten im Auftrag ist, wird auch der Auftrag gelöscht.</>}
            </p>
            <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
              <button className="btn btn-ghost" onClick={() => setConfirmDelete(false)}>Abbrechen</button>
              <button className="btn" style={{ background: 'var(--danger)', color: '#fff' }}
                onClick={async () => {
                  setConfirmDelete(false);
                  if (onDeleteGutachten) await onDeleteGutachten(g.id);
                }}
              >Endgültig löschen</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

const GutachtenTabs = ({ active, onChange }) => (
  <div className="gutachten-tabs">
    <button
      className={`gutachten-tab ${active === 'unterlagen' ? 'active' : ''}`}
      onClick={() => onChange('unterlagen')}
    >Unterlagen</button>
    <button
      className={`gutachten-tab ${active === 'stammdaten' ? 'active' : ''}`}
      onClick={() => onChange('stammdaten')}
    >Stammdaten</button>
    <button
      className={`gutachten-tab ${active === 'ortstermin' ? 'active' : ''}`}
      onClick={() => onChange('ortstermin')}
    >Ortstermin</button>
    <button
      className={`gutachten-tab ${active === 'fotos' ? 'active' : ''}`}
      onClick={() => onChange('fotos')}
    >Fotos</button>
    <button
      className={`gutachten-tab ${active === 'entwurf' ? 'active' : ''}`}
      onClick={() => onChange('entwurf')}
    >Entwurf</button>
    <button className="gutachten-tab disabled">Gutachten</button>
  </div>
);

const OBJEKTTYP_OPTIONS = [
  { value: 'efh', label: 'Einfamilienhaus' },
  { value: 'etw', label: 'Eigentumswohnung' },
  { value: 'mfh', label: 'Mehrfamilienhaus' },
  { value: 'dhh', label: 'Doppelhaushälfte' },
  { value: 'gewerbe', label: 'Gewerbe' },
  { value: 'grundstueck', label: 'Grundstück' },
  { value: 'stellplatz', label: 'Stellplatz / TG' },
  { value: 'keller', label: 'Kellerraum' },
];

// ── Auftrags-Bearbeitungsstatus (manueller Workflow) ──
// Fünf feste Stufen, manuell per Dropdown gesetzt. Gespeichert in
// auftraege.bearbeitungsstatus (stabile value-Keys, NICHT die Nummer —
// so bricht nichts, falls Stufen später umsortiert werden).
// Nuancierte Farbprogression: neutral → blau → orange → violett → grün.
// fg = Textfarbe, bg = dezenter Hintergrund (Badge-Stil).
const AUFTRAG_STATUS_OPTIONS = [
  { value: 'angelegt',     label: '01 Auftrag angelegt',        short: '01 Angelegt',      fg: '#475569', bg: '#F1F5F9', ord: 0 },
  { value: 'unterlagen',   label: '02 Unterlagen sammeln',     short: '02 Unterlagen',    fg: '#0E7490', bg: '#E0F2F5', ord: 1 },
  { value: 'ortstermin',   label: '03 Ortstermin vereinbart',  short: '03 Ortstermin',    fg: '#003B71', bg: '#E3ECF5', ord: 2 },
  { value: 'bearbeitung',  label: '04 Gutachten in Bearbeitung', short: '04 In Bearbeitung', fg: '#B45309', bg: '#FDF0E2', ord: 3 },
  { value: 'rechnung',     label: '05 Rechnung gestellt',      short: '05 Rechnung',      fg: '#6D28D9', bg: '#F0EAFB', ord: 4 },
  { value: 'abgeschlossen', label: '06 Auftrag abgeschlossen', short: '06 Abgeschlossen', fg: '#15803D', bg: '#E7F4EC', ord: 5 },
];
const AUFTRAG_STATUS_BY_VALUE = Object.fromEntries(AUFTRAG_STATUS_OPTIONS.map(o => [o.value, o]));


// ══════════════════════════════════════════════════════════════════
// ObjektDokumentDaten — zeigt die aus Dokumenten extrahierten Datenpunkte
// gesammelt beim Objekt, gruppiert nach Dokument. Editierbar (schreibt
// zurück ans jeweilige Dokument, wie das DokumentFelderPanel).
// So sieht man pro Objekt alle vorliegenden Daten, ohne jedes Dokument
// einzeln im Viewer öffnen zu müssen.
// ══════════════════════════════════════════════════════════════════
const ObjektDokumentDaten = ({ dokumente, session, workerUrl, onOpenDokument, onRefresh }) => {
  const showToast = useToast();
  const [open, setOpen] = useState(false);
  const [localFieldsMap, setLocalFieldsMap] = useState({}); // dokId → gemergte fields

  const SKIP = new Set(['beteiligte', 'eigentuemer', 'versteigerungsobjekte', 'einheiten', 'titel_vorschlag', 'notizen']);
  // Vollständige Feldliste eines Dokuments: erwartete Felder (Schema) +
  // zusätzlich vorhandene. Zeigt auch nicht-extrahierte (leere) Felder.
  const keysFuer = (typRaw, fields) => {
    const erwartet = expectedFieldsFor(typRaw).filter(k => !SKIP.has(k));
    const vorhanden = fields
      ? Object.keys(fields).filter(k => !SKIP.has(k) && fields[k] != null && typeof fields[k] !== 'object')
      : [];
    const result = [...erwartet];
    for (const k of vorhanden) if (!result.includes(k)) result.push(k);
    return result;
  };

  // Nur Dokumente, die NICHT bereits per applyRouting in die Stammdaten-
  // Sektionen fließen (sonst Redundanz).
  const docsM = (dokumente || [])
    .filter(d => !typHatRouting(d.typ_raw))
    .map(d => {
      const fields = localFieldsMap[d.id] || d.extracted_fields?.fields || null;
      const keys = keysFuer(d.typ_raw, fields);
      return keys.length > 0 ? { d, fields, keys } : null;
    })
    .filter(Boolean);

  if (docsM.length === 0) return null;

  // Feldschlüssel → thematische Gruppe (rein für die Darstellung).
  // Erste passende Regel gewinnt; Reihenfolge = Anzeigereihenfolge.
  const FELD_THEMA_REGELN = [
    ['Bewertung', /^(verkehrswert|wertermittlung|qualitaetsstichtag|zuschlag)/i],
    ['Miete & Ertrag', /(miete|vermietung|vergleichsmiet|pacht|ertrag|nettokalt|staffel|kuendig|leerstand)/i],
    ['WEG-Verwaltung', /(verwaltung|verwalter|hausgeld|ruecklage|instandhaltung|sonderumlage|beschluss|beschluesse|ansprechpartner|telefon|modernisierung|gemeinschaftseigentum)/i],
    ['Versicherung', /(versicher|praemie|selbstbeteiligung)/i],
    ['Energie & Technik', /(energie|effizienz|endenergie|primaerenergie|heizung|brenner|tank|fabrikat|messbescheinigung|maengel|ausweis|gueltig_bis|registriernummer|erneuerbare)/i],
    ['Baurecht & Erschließung', /(bplan|fnp|baulinie|baugrenze|bebauungs|veraenderungssperre|satzungen|erschliessung|strassen|herstellungsbeitraege|entwasserung|grundstuecksentwaesserung|gemeinderecht|paragraph_34_35|baurecht|grz|gfz|vollgeschosse|bauweise|dachform|dachneigung|art_bauliche_nutzung)/i],
    ['Bodenwert', /(bodenrichtwert|^brw_)/i],
    ['Grundstück & Grundbuch', /(flur|flurstueck|gemarkung|groesse_qm|grundbuch|bestandsverzeichnis|abteilung_|abt_\d|herrschvermerk|wirtschaftsart|mea|sondereigentum|bruchteilseigentum|belastungen|kaufvertrag)/i],
    ['Altlasten & Denkmalschutz', /(altlast|denkmal|kataster|eintrag_text|auffaelligkeiten)/i],
    ['Gebäude', /(baujahr|objekttyp|objektadresse|wohnflaeche|lage_im_gebaeude|geschoss)/i],
    ['Auftrag & Verfahren', /(aktenzeichen|gerichtstyp|auftrag|verfahren|^sache$|wegen|kostenvorschuss|ausfertigungen|abgabe_extern|beschlussdatum|^zweck$)/i],
  ];
  const FALLBACK_THEMA = 'Weitere Angaben';
  const themaFuer = (k) => {
    for (const [label, re] of FELD_THEMA_REGELN) if (re.test(k)) return label;
    return FALLBACK_THEMA;
  };

  // Alle Felder flach über die Dokumente, klassifiziert nach Thema. Jeder
  // Eintrag merkt sich sein Quelldokument (für Speichern + Beleg-Anzeige).
  const themaGruppen = {};
  for (const { d, fields, keys } of docsM) {
    for (const k of keys) {
      const t = themaFuer(k);
      (themaGruppen[t] = themaGruppen[t] || []).push({
        dokId: d.id, typ: d.typ, key: k,
        label: FELD_LABELS[k] || k,
        value: (fields && fields[k] != null) ? fields[k] : null,
      });
    }
  }
  const themaOrder = [...FELD_THEMA_REGELN.map(r => r[0]), FALLBACK_THEMA];
  const gruppen = themaOrder.filter(t => themaGruppen[t]).map(t => [t, themaGruppen[t]]);

  // Einzelnes Feld speichern (zurück an sein Quelldokument). Wirft bei Fehler,
  // damit EditableField die Meldung inline anzeigt. felder-update unverändert.
  const saveField = async (dokId, key, neu) => {
    const res = await fetch(`${workerUrl}/api/dokument/felder-update`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
      body: JSON.stringify({ dokument_id: dokId, fields: { [key]: (neu == null || neu === '') ? null : neu } }),
    });
    if (!res.ok) {
      let msg = 'Speichern fehlgeschlagen';
      try { const e = await res.json(); if (e.error) msg = e.error; } catch {}
      throw new Error(msg);
    }
    const result = await res.json();
    if (result?.fields) setLocalFieldsMap(m => ({ ...m, [dokId]: result.fields }));
    showToast('Datenpunkt gespeichert', 'success');
    if (onRefresh) onRefresh();
  };

  const dokAnzahl = docsM.length;

  return (
    <div style={{
      marginBottom: 'var(--space-3)', background: 'var(--surface)',
      border: '1px solid var(--border-light)', borderRadius: 'var(--radius-lg)',
      boxShadow: 'var(--shadow-subtle)', overflow: 'hidden',
    }}>
      <button type="button" onClick={() => setOpen(!open)} style={{
        display: 'flex', alignItems: 'center', gap: 'var(--space-3)', width: '100%',
        padding: '13px 16px', background: open ? 'var(--surface-raised)' : 'var(--surface)',
        border: 'none', cursor: 'pointer', textAlign: 'left',
        transition: 'background var(--dur-fast) var(--ease-out)',
      }}>
        <span style={{
          width: 34, height: 34, borderRadius: 'var(--radius-md)', flexShrink: 0,
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          background: 'var(--surface-blue)', color: 'var(--vl-blue)',
        }}>
          <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="9" y1="13" x2="15" y2="13" /><line x1="9" y1="17" x2="13" y2="17" />
          </svg>
        </span>
        <span style={{ flex: 1, minWidth: 0 }}>
          <span style={{ display: 'block', fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>Weitere extrahierte Datenpunkte</span>
          <span style={{ display: 'block', fontSize: 12, color: 'var(--text-tertiary)', marginTop: 1 }}>
            Aus {dokAnzahl} {dokAnzahl === 1 ? 'Dokument' : 'Dokumenten'} · thematisch geordnet, direkt bearbeitbar
          </span>
        </span>
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
          style={{ color: 'var(--text-tertiary)', flexShrink: 0, transform: open ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform var(--dur-base) var(--ease-out)' }}>
          <polyline points="9 18 15 12 9 6" />
        </svg>
      </button>
      <div style={{ display: 'grid', gridTemplateRows: open ? '1fr' : '0fr', transition: 'grid-template-rows var(--dur-base) var(--ease-out)' }}>
        <div style={{ overflow: 'hidden', minHeight: 0 }}>
        <div style={{ padding: '4px 14px 14px' }}>
          {gruppen.map(([thema, items]) => (
            <div key={thema} style={{ marginBottom: 'var(--space-3)' }}>
              <div style={{
                fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
                letterSpacing: '0.08em', color: 'var(--vl-blue)', margin: '8px 0 6px 2px',
              }}>{thema}</div>
              <ObjTable>
                {items.map(it => {
                  const lang = typeof it.value === 'string' && it.value.length > 60;
                  return (
                    <ObjTableRow
                      key={it.dokId + ':' + it.key}
                      label={it.label}
                      herkunft={{ dokument_id: it.dokId, titel: it.typ }}
                      onOpenInline={onOpenDokument}
                    >
                      <EditableField
                        value={it.value}
                        type={lang ? 'textarea' : 'text'}
                        onSave={(v) => saveField(it.dokId, it.key, v)}
                        disabled={!session}
                      />
                    </ObjTableRow>
                  );
                })}
              </ObjTable>
            </div>
          ))}
        </div>
        </div>
      </div>
    </div>
  );
};

// Anspringbare Kerndaten — gemeinsam von der gutachtenweiten Befehls-Palette
// (GutachtenView) und dem Feld-Sprung in StammdatenTab genutzt.
const JUMP_FELD_ORDER = ['adresse', 'bezeichnung', 'objekttyp', 'baujahr', 'wohnflaeche', 'gemarkung', 'flur', 'flurstueck', 'groesse_qm', 'art_bauliche_nutzung', 'grz', 'gfz', 'bodenrichtwert'];
const FELD_LABEL = {
  adresse: 'Adresse', bezeichnung: 'Bezeichnung', objekttyp: 'Objektart', baujahr: 'Baujahr',
  wohnflaeche: 'Wohnfläche', gemarkung: 'Gemarkung', flur: 'Flur', flurstueck: 'Flurstück',
  groesse_qm: 'Grundstücksgröße', art_bauliche_nutzung: 'Art der baulichen Nutzung',
  grz: 'GRZ', gfz: 'GFZ', bodenrichtwert: 'Bodenrichtwert',
};
const FELD_SEKTION_MAP = {
  adresse: 'Gebäude', bezeichnung: 'Gebäude', objekttyp: 'Gebäude', baujahr: 'Gebäude', wohnflaeche: 'Gebäude',
  gemarkung: 'Grundstück', flur: 'Grundstück', flurstueck: 'Grundstück', groesse_qm: 'Grundstück',
  art_bauliche_nutzung: 'Planungsrecht', grz: 'Planungsrecht', gfz: 'Planungsrecht',
  bodenrichtwert: 'Bodenwert',
};

// ── Befehls-Palette (⌘K / Strg-K): schnelles Springen zu Feldern/Objekten ──
// Gesteuert über open/onClose. items = [{ id, group, label, sub, kind:'feld'|'objekt',
// filled?, current?, keywords, run }]. Hält eigene Suche/Auswahl + Tastatursteuerung.
const CommandPalette = ({ open, onClose, items }) => {
  const [q, setQ] = useState('');
  const [idx, setIdx] = useState(0);
  const inputRef = useRef(null);
  const listRef = useRef(null);

  useEffect(() => {
    if (!open) return;
    setQ(''); setIdx(0);
    const t = setTimeout(() => { if (inputRef.current) inputRef.current.focus(); }, 30);
    return () => clearTimeout(t);
  }, [open]);

  const filtered = useMemo(() => {
    const term = q.trim().toLowerCase();
    if (!term) return items;
    return items.filter(it => ((it.label || '') + ' ' + (it.sub || '') + ' ' + (it.keywords || '')).toLowerCase().includes(term));
  }, [q, items]);

  useEffect(() => { setIdx(i => Math.min(i, Math.max(0, filtered.length - 1))); }, [filtered.length]);
  useEffect(() => {
    if (!open || !listRef.current) return;
    const el = listRef.current.querySelector('[data-idx="' + idx + '"]');
    if (el) el.scrollIntoView({ block: 'nearest' });
  }, [idx, open]);

  if (!open) return null;

  const choose = (it) => { if (!it) return; onClose(); setTimeout(() => it.run(), 0); };
  const onInputKey = (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); setIdx(i => Math.min(i + 1, filtered.length - 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setIdx(i => Math.max(i - 1, 0)); }
    else if (e.key === 'Enter') { e.preventDefault(); choose(filtered[idx]); }
    else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onClose(); }
  };

  let lastGroup = null;
  return (
    <div className="cmdk-backdrop" onMouseDown={onClose}>
      <div className="cmdk-panel" onMouseDown={e => e.stopPropagation()} role="dialog" aria-modal="true">
        <div className="cmdk-input-wrap">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></svg>
          <input ref={inputRef} className="cmdk-input" value={q} placeholder="Springen zu… (Feld oder Objekt)"
            onChange={e => { setQ(e.target.value); setIdx(0); }} onKeyDown={onInputKey} />
        </div>
        <div className="cmdk-list" ref={listRef}>
          {filtered.length === 0 && <div className="cmdk-empty">Nichts gefunden</div>}
          {filtered.map((it, i) => {
            const header = it.group !== lastGroup ? it.group : null;
            lastGroup = it.group;
            return (
              <Fragment key={it.id}>
                {header && <div className="cmdk-group">{header}</div>}
                <button type="button" data-idx={i} className={'cmdk-item' + (i === idx ? ' active' : '')}
                  onMouseEnter={() => setIdx(i)} onClick={() => choose(it)}>
                  {it.kind === 'feld'
                    ? <span className="cmdk-dot" style={{ background: it.filled ? 'var(--success)' : 'var(--text-tertiary)' }} />
                    : <span className="cmdk-dot cmdk-dot-obj" style={{ background: it.current ? 'var(--vl-blue)' : 'transparent', border: '2px solid var(--vl-blue)' }} />}
                  <span className="cmdk-item-label">{it.label}</span>
                  {it.sub && <span className="cmdk-item-sub">{it.sub}</span>}
                </button>
              </Fragment>
            );
          })}
        </div>
        <div className="cmdk-foot">
          <span><kbd>↑</kbd><kbd>↓</kbd> wählen</span>
          <span><kbd>↵</kbd> öffnen</span>
          <span><kbd>esc</kbd> schließen</span>
        </div>
      </div>
    </div>
  );
};

const StammdatenTab = ({
  g, aktivesObjekt, onSwitchObjekt,
  herkunft, dokumente,
  session, workerUrl,
  onRefresh, pendingFeld, onFeldConsumed,
}) => {
  const multiObj = g.objekte.length > 1;
  // Index sicher in den gültigen Bereich klemmen. Nach dem Anlegen wird kurz
  // auf den neuen Index umgeschaltet, bevor der Refresh das Objekt liefert —
  // ohne Klemmung wäre obj dann undefined und das Rendern stürzte ab (Weißbild).
  const aktivIdx = g.objekte.length ? Math.max(0, Math.min(aktivesObjekt, g.objekte.length - 1)) : 0;
  const obj = g.objekte[aktivIdx] || null;

  // Inline-Viewer-State
  const [viewerDokId, setViewerDokId] = useState(null);
  const viewerActive = !!viewerDokId;
  // PDF-Beleg maximieren: linke Datenspalte ausblenden, PDF in voller Breite
  const [pdfMax, setPdfMax] = useState(false);
  useEffect(() => { if (!viewerDokId) setPdfMax(false); }, [viewerDokId]);

  // ── Sprung zu einem Feld (aus „Noch offen"-Chips / Tastatur „nächste Lücke") ──
  const [jumpKey, setJumpKey] = useState(null);
  const [jumpNonce, setJumpNonce] = useState(0);
  // Reihenfolge der anspringbaren Kerndaten (= Render-Reihenfolge)
  const JUMP_FELD_ORDER = ['adresse', 'bezeichnung', 'objekttyp', 'baujahr', 'wohnflaeche', 'gemarkung', 'flur', 'flurstueck', 'groesse_qm', 'art_bauliche_nutzung', 'grz', 'gfz', 'bodenrichtwert'];
  const feldWert = (key) => key === 'adresse' ? g.adresse : (obj ? obj[key] : null);
  const jumpToFeld = (key) => { if (!key) return; setJumpKey(key); setJumpNonce(n => n + 1); };
  const nextOpenFeld = (currentKey) => {
    const i = JUMP_FELD_ORDER.indexOf(currentKey);
    for (let j = i + 1; j < JUMP_FELD_ORDER.length; j++) {
      const w = feldWert(JUMP_FELD_ORDER[j]);
      if (w == null || w === '') return JUMP_FELD_ORDER[j];
    }
    return null;
  };
  // Welches Feld gehört in welche Datengruppe (zum Aufklappen beim Sprung)
  const FELD_SEKTION = {
    adresse: 'Gebäude', bezeichnung: 'Gebäude', objekttyp: 'Gebäude', baujahr: 'Gebäude', wohnflaeche: 'Gebäude',
    gemarkung: 'Grundstück', flur: 'Grundstück', flurstueck: 'Grundstück', groesse_qm: 'Grundstück',
    art_bauliche_nutzung: 'Planungsrecht', grz: 'Planungsrecht', gfz: 'Planungsrecht',
    bodenrichtwert: 'Bodenwert',
  };
  const sektionSignal = (titel) => (jumpKey && FELD_SEKTION[jumpKey] === titel ? jumpNonce : 0);
  // Beim Sprung: Zielgruppe ggf. aufklappen lassen, dann ins Bild scrollen +
  // kurz hervorheben (das Öffnen des Feldes macht EditableField via openSignal).
  useEffect(() => {
    if (!jumpNonce || !jumpKey) return;
    let raf1, raf2, t;
    const tryScroll = () => {
      const el = document.getElementById('feld-' + jumpKey);
      if (!el) return false;
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      el.classList.add('feld-pulse');
      t = setTimeout(() => el.classList.remove('feld-pulse'), 1400);
      return true;
    };
    raf1 = requestAnimationFrame(() => { if (!tryScroll()) raf2 = requestAnimationFrame(tryScroll); });
    return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); clearTimeout(t); };
  }, [jumpNonce, jumpKey]);

  // Pfeiltasten ← / → wechseln das Objekt (nur Mehr-Objekt, nicht beim Tippen)
  useEffect(() => {
    if (!multiObj) return;
    const onKey = (e) => {
      const ae = document.activeElement;
      const tag = (ae?.tagName || '').toUpperCase();
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || ae?.isContentEditable) return;
      if (e.key === 'ArrowRight' && aktivesObjekt < g.objekte.length - 1) {
        e.preventDefault(); onSwitchObjekt(aktivesObjekt + 1); window.scrollTo({ top: 0, behavior: 'smooth' });
      } else if (e.key === 'ArrowLeft' && aktivesObjekt > 0) {
        e.preventDefault(); onSwitchObjekt(aktivesObjekt - 1); window.scrollTo({ top: 0, behavior: 'smooth' });
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [multiObj, aktivesObjekt, g.objekte.length, onSwitchObjekt]);

  // ── Sprung-Anforderung aus der gutachtenweiten Befehls-Palette ───────────
  // Kommt der Wunsch von einem anderen Reiter, ist dieser Tab gerade frisch
  // gemountet. Wir springen einmalig und melden „verbraucht", damit ein
  // späterer Wechsel zurück auf diesen Reiter nicht erneut springt.
  useEffect(() => {
    if (!pendingFeld) return;
    jumpToFeld(pendingFeld);
    onFeldConsumed && onFeldConsumed();
  }, [pendingFeld]);

  // Scope-Filter: Gutachten-Dokumente (dieses Gutachten) + Objekt-Dokumente
  // (aktives Objekt) + Auftragsdokumente (Beschluss/Anschreiben — enthalten
  // Felder die ins Gutachten/Objekt fließen, z.B. Flurstück aus Beschluss).
  const relevanteDokumente = useMemo(() => {
    const all = dokumente || [];
    // Bei multiObj alle Objekte des Gutachtens berücksichtigen (alle untereinander sichtbar),
    // bei single-Obj nur das aktive Objekt
    const objektIds = new Set(
      multiObj ? (g.objekte || []).map(o => o.id).filter(Boolean)
               : (obj?.id ? [obj.id] : [])
    );
    return all.filter(d => {
      if (d.scope === 'auftrag') return true;
      if (d.scope === 'gutachten' && d.gutachten_id === g.id) return true;
      if (d.scope === 'objekt' && d.objekt_id && objektIds.has(d.objekt_id)) return true;
      return false;
    });
  }, [dokumente, g.id, obj?.id, multiObj, g.objekte]);

  // Herkunft für Gutachten-Felder
  const gh = (feld) => herkunft?.[`gutachten:${g.id}:${feld}`];
  // Herkunft für Objekt-Felder: dynamisch — wird in der pro-Objekt-Schleife
  // unten neu definiert (mit dem jeweils aktuellen Objekt im Closure)

  const handleOpenInline = useCallback(async (dokumentId) => {
    // Mobile: Dokument im neuen Tab öffnen statt Split-View
    if (window.innerWidth < 768) {
      try {
        const res = await fetch(
          `${workerUrl}/api/document-url?id=${encodeURIComponent(dokumentId)}`,
          { headers: { Authorization: `Bearer ${session.access_token}` } }
        );
        if (res.ok) { const { url } = await res.json(); window.open(url, '_blank'); return; }
      } catch {}
    }
    setViewerDokId(dokumentId);
  }, [dokumente, session, workerUrl]);

  // Patch-Helfer für Gutachten-Felder
  const updateGutachten = async (feld, wert) => {
    if (!g.id) throw new Error('Gutachten-ID fehlt');
    await apiPatchRow('gutachten', g.id, { [feld]: wert }, session, workerUrl);
    onRefresh && onRefresh();
  };
  // updateObjekt: wird in der pro-Objekt-Schleife unten neu definiert

  const canEdit = !!session;
  const canEditGutachten = canEdit && !!g.id;
  // canEditObjekt und groesseNumber: in der pro-Objekt-Schleife unten
  // Aktives Objekt aktualisieren — für den klickbaren Objektnamen im Hero
  // (liegt außerhalb der pro-Objekt-Schleife). Speichert wie updateObjekt.
  const updateAktivObjekt = async (feld, wert) => {
    if (!obj?.id) throw new Error('Objekt-ID fehlt');
    const numericFields = new Set(['groesse_qm', 'wohnflaeche', 'baujahr', 'wertansatz', 'sort_order']);
    const normalized = numericFields.has(feld) && wert != null && wert !== ''
      ? Number(wert) : (wert == null || wert === '' ? null : wert);
    await apiPatchRow('bewertungsobjekte', obj.id, { [feld]: normalized }, session, workerUrl);
    onRefresh && onRefresh();
  };

  // Add/Delete-Objekt-State
  const [addObjektOpen, setAddObjektOpen] = useState(false);
  const [newObjBezeichnung, setNewObjBezeichnung] = useState('');
  const [newObjTyp, setNewObjTyp] = useState('etw');
  const [addObjSaving, setAddObjSaving] = useState(false);
  const [addObjError, setAddObjError] = useState(null);
  const [deleteObjConfirm, setDeleteObjConfirm] = useState(null);

  const handleAddObjekt = async () => {
    if (!newObjBezeichnung.trim()) return;
    setAddObjSaving(true); setAddObjError(null);
    try {
      const result = await apiInsertRow('bewertungsobjekte', {
        gutachten_id: g.id,
        bezeichnung: newObjBezeichnung.trim(),
        objekttyp: newObjTyp || null,
      }, session, workerUrl);
      setNewObjBezeichnung(''); setNewObjTyp('etw');
      setAddObjektOpen(false);
      onRefresh && onRefresh();
      // Zum neu angelegten Objekt springen: es ist das letzte in der Liste
      // nach dem refresh. Der Parent-Switcher-Index bleibt, aber der refresh
      // bringt das neue Objekt in g.objekte; wir springen idx=aktuelleLänge.
      onSwitchObjekt(g.objekte.length);
    } catch (err) {
      setAddObjError(err.message || String(err));
    } finally {
      setAddObjSaving(false);
    }
  };

  const handleDeleteObjekt = () => {
    if (!obj?.id) return;
    if (g.objekte.length <= 1) {
      alert('Das letzte Bewertungsobjekt eines Gutachtens kann nicht gelöscht werden. Lege erst ein weiteres Objekt an oder lösche den ganzen Auftrag.');
      return;
    }
    setDeleteObjConfirm({ obj, busy: false, error: null });
  };
  const executeDeleteObjekt = async () => {
    if (!deleteObjConfirm) return;
    setDeleteObjConfirm(s => ({ ...s, busy: true, error: null }));
    try {
      await apiDeleteRow('bewertungsobjekte', deleteObjConfirm.obj.id, session, workerUrl);
      setDeleteObjConfirm(null);
      // Auf das erste Objekt springen, dann refresh
      onSwitchObjekt(0);
      onRefresh && onRefresh();
    } catch (err) {
      setDeleteObjConfirm(s => ({ ...s, busy: false, error: err.message || String(err) }));
    }
  };

  // ── Daten-Vollständigkeit berechnen ──
  const vollstaendigkeit = useMemo(() => {
    const sections = [];
    const v = (val) => val != null && val !== '' && val !== 0;

    sections.push({
      label: 'Lage', filled: [g.adresse].filter(v).length, total: 1,
      missing: !v(g.adresse) ? [{ label: 'Adresse', key: 'adresse' }] : [],
    });

    if (obj) {
      sections.push({
        label: 'Gebäude',
        filled: [obj.bezeichnung, obj.objekttyp, obj.baujahr, obj.wohnflaeche].filter(v).length,
        total: 4,
        missing: [
          !v(obj.bezeichnung) && { label: 'Bezeichnung', key: 'bezeichnung' },
          !v(obj.objekttyp) && { label: 'Objekttyp', key: 'objekttyp' },
          !v(obj.baujahr) && { label: 'Baujahr', key: 'baujahr' },
          !v(obj.wohnflaeche) && { label: 'Wohnfläche', key: 'wohnflaeche' },
        ].filter(Boolean),
      });
      sections.push({
        label: 'Grundstück',
        filled: [obj.gemarkung, obj.flur, obj.flurstueck, obj.groesse_qm].filter(v).length,
        total: 4,
        missing: [
          !v(obj.gemarkung) && { label: 'Gemarkung', key: 'gemarkung' },
          !v(obj.flur) && { label: 'Flur', key: 'flur' },
          !v(obj.flurstueck) && { label: 'Flurstück', key: 'flurstueck' },
          !v(obj.groesse_qm) && { label: 'Fläche', key: 'groesse_qm' },
        ].filter(Boolean),
      });
      sections.push({
        label: 'Planung',
        filled: [obj.art_bauliche_nutzung, obj.grz, obj.gfz, obj.bodenrichtwert].filter(v).length,
        total: 4,
        missing: [
          !v(obj.art_bauliche_nutzung) && { label: 'Nutzung', key: 'art_bauliche_nutzung' },
          !v(obj.grz) && { label: 'GRZ', key: 'grz' },
          !v(obj.gfz) && { label: 'GFZ', key: 'gfz' },
          !v(obj.bodenrichtwert) && { label: 'BRW', key: 'bodenrichtwert' },
        ].filter(Boolean),
      });
    }
    return sections;
  }, [g, obj]);

  const dataColumn = (
    <>
      {/* Objektdaten-Überblick: Gesamt-Vollständigkeit + was noch offen ist */}
      {(() => {
        const totFilled = vollstaendigkeit.reduce((s, x) => s + x.filled, 0);
        const totTotal = vollstaendigkeit.reduce((s, x) => s + x.total, 0);
        const pct = totTotal > 0 ? totFilled / totTotal : 0;
        const missingItems = vollstaendigkeit.flatMap(s => s.missing.map(m => ({ label: m.label, key: m.key, sektion: s.label })));
        const heroColor = pct >= 1 ? 'var(--success)' : pct > 0 ? 'var(--warning)' : 'var(--text-tertiary)';
        const RR = 26, CC = 2 * Math.PI * RR;
        const objTypLabel = obj ? (OBJEKTTYP_OPTIONS.find(o => o.value === obj.objekttyp || o.label === obj.objekttyp)?.label || obj.objekttyp) : null;
        return (
          <div style={{
            background: 'var(--surface)', border: '1px solid var(--border-light)',
            borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-card)',
            padding: 'var(--space-5)', marginBottom: 'var(--space-4)',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-5)', flexWrap: 'wrap' }}>
              <div style={{ position: 'relative', width: 64, height: 64, flexShrink: 0 }}>
                <svg width="64" height="64" viewBox="0 0 64 64" style={{ transform: 'rotate(-90deg)' }}>
                  <circle cx="32" cy="32" r={RR} fill="none" stroke="var(--border-light)" strokeWidth="6" />
                  <circle cx="32" cy="32" r={RR} fill="none" stroke={heroColor} strokeWidth="6" strokeLinecap="round"
                    strokeDasharray={CC} strokeDashoffset={CC * (1 - pct)}
                    style={{ transition: 'stroke-dashoffset var(--dur-slow) var(--ease-out), stroke var(--dur-base)' }} />
                </svg>
                <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, fontWeight: 700, color: 'var(--text-primary)' }}>
                  {Math.round(pct * 100)}%
                </div>
              </div>
              <div style={{ flex: 1, minWidth: 160 }}>
                {multiObj && (
                  <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--vl-orange-dark)', marginBottom: 3 }}>
                    Objekt {aktivesObjekt + 1} von {g.objekte.length}
                  </div>
                )}
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                  <EditableField
                    value={obj?.bezeichnung}
                    onSave={(v) => updateAktivObjekt('bezeichnung', v)}
                    disabled={!canEdit || !obj?.id}
                    placeholder="Objektname…"
                    style={{ fontSize: 17, fontWeight: 700, color: 'var(--text-primary)', letterSpacing: '-0.01em' }}
                    inputStyle={{ width: 'auto', minWidth: 200, maxWidth: 380, fontSize: 17, fontWeight: 700 }}
                  />
                  {objTypLabel && <span className="pill pill-blue" style={{ fontSize: 11 }}>{objTypLabel}</span>}
                </div>
                <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 }}>
                  {totFilled} von {totTotal} Kerndaten gesichert{missingItems.length === 0 ? ' · alles erfasst' : ''}
                </div>
              </div>
              <div style={{ display: 'flex', gap: 'var(--space-4)', flexWrap: 'wrap' }}>
                {vollstaendigkeit.map(sec => {
                  const p = sec.total > 0 ? sec.filled / sec.total : 0;
                  const c = p >= 1 ? 'var(--success)' : p > 0 ? 'var(--warning)' : 'var(--text-tertiary)';
                  const firstOpen = sec.missing[0];
                  return (
                    <button key={sec.label} type="button"
                      onClick={() => firstOpen && jumpToFeld(firstOpen.key)}
                      disabled={!firstOpen}
                      title={firstOpen ? `Zu „${firstOpen.label}" springen` : 'vollständig'}
                      style={{ minWidth: 92, textAlign: 'left', background: 'none', border: 'none', padding: 0, cursor: firstOpen ? 'pointer' : 'default' }}>
                      <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, marginBottom: 4 }}>
                        <span style={{ color: 'var(--text-secondary)', fontWeight: 500 }}>{sec.label}</span>
                        <span style={{ color: c, fontWeight: 700 }}>{sec.filled}/{sec.total}</span>
                      </div>
                      <div style={{ height: 4, background: 'var(--border-light)', borderRadius: 'var(--radius-full)', overflow: 'hidden' }}>
                        <div style={{ height: '100%', width: `${p * 100}%`, background: c, borderRadius: 'var(--radius-full)', transition: 'width var(--dur-slow) var(--ease-out)' }} />
                      </div>
                    </button>
                  );
                })}
              </div>
            </div>
            {/* Mini-Legende: erklärt die Ring-/Balkenfarben ohne Worte */}
            <div style={{ marginTop: 'var(--space-3)', display: 'flex', alignItems: 'center', gap: 13, flexWrap: 'wrap', fontSize: 11, color: 'var(--text-tertiary)' }}>
              {[['var(--text-tertiary)', 'leer'], ['var(--warning)', 'teilweise'], ['var(--success)', 'vollständig']].map(([col, lab]) => (
                <span key={lab} style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
                  <span style={{ width: 9, height: 9, borderRadius: '50%', border: `2.5px solid ${col}`, flexShrink: 0 }} />
                  {lab}
                </span>
              ))}
            </div>
            {missingItems.length > 0 && (
              <div style={{ marginTop: 'var(--space-4)', paddingTop: 'var(--space-3)', borderTop: '1px solid var(--border-light)', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                <span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-tertiary)' }}>Noch offen</span>
                {missingItems.slice(0, 8).map((m, i) => (
                  <button key={i} type="button"
                    onClick={() => jumpToFeld(m.key)}
                    title={`„${m.label}" ausfüllen`}
                    style={{
                      display: 'inline-flex', alignItems: 'center', gap: 4,
                      fontSize: 12, fontWeight: 600, color: 'var(--warning)',
                      background: 'var(--warning-bg)', border: '1px solid #F2D5A8',
                      padding: '3px 9px 3px 11px', borderRadius: 'var(--radius-full)', cursor: 'pointer',
                      transition: 'background var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out)',
                    }}
                    onMouseEnter={(e) => { e.currentTarget.style.background = '#FCEBD2'; e.currentTarget.style.transform = 'translateY(-1px)'; }}
                    onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--warning-bg)'; e.currentTarget.style.transform = 'none'; }}
                  >
                    {m.label}
                    <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></svg>
                  </button>
                ))}
                {missingItems.length > 8 && (
                  <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-tertiary)' }}>
                    +{missingItems.length - 8} weitere
                  </span>
                )}
              </div>
            )}
          </div>
        );
      })()}

      {/* Objekt-Nav: bei multiObj Quick-Scroll zu den Objekt-Sektionen (die sind
          untereinander gerendert), bei single-Objekt nur "+ Weiteres Objekt"-Link. */}
      <div className="objekt-switcher" style={{ flexWrap: 'wrap' }}>
        {multiObj && g.objekte.map((o, i) => (
          <div key={o.id || i} style={{ position: 'relative', display: 'inline-flex' }}>
            <button
              className={`objekt-chip ${i === aktivesObjekt ? 'active' : ''}`}
              onClick={() => {
                onSwitchObjekt(i);
                if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
              }}
              title={`Objekt „${o.bezeichnung}" anzeigen`}
              style={{ paddingRight: i === aktivesObjekt ? 28 : undefined }}
            >
              {o.bezeichnung}
            </button>
            {i === aktivesObjekt && canEdit && (
              <button
                type="button"
                onClick={handleDeleteObjekt}
                title="Dieses Objekt löschen"
                style={{
                  position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)',
                  background: 'none', border: 'none',
                  color: 'var(--text-tertiary)', cursor: 'pointer',
                  padding: '2px 4px', fontSize: 14, lineHeight: 1,
                }}
                onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--danger)'; }}
                onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--text-tertiary)'; }}
              >×</button>
            )}
          </div>
        ))}
        {canEdit && g.id && (
          <button
            className="objekt-chip objekt-chip-add"
            onClick={() => { setAddObjektOpen(true); setAddObjError(null); }}
            style={!multiObj ? { fontSize: 12, color: 'var(--text-tertiary)' } : {}}
          >
            + Weiteres Objekt
          </button>
        )}
      </div>

      {addObjektOpen && (
        <div className="card" style={{
          marginBottom: 'var(--space-4)',
          borderLeft: '3px solid var(--vl-orange)',
        }}>
          <div className="card-header">
            <span className="card-title">Neues Bewertungsobjekt</span>
          </div>
          <div style={{ padding: 'var(--space-4)', display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
            <div>
              <label style={formLabelStyle()}>Bezeichnung *</label>
              <input
                type="text"
                value={newObjBezeichnung}
                onChange={(e) => setNewObjBezeichnung(e.target.value)}
                placeholder='z.B. "Whg Nr. 2, 1. OG" oder "Grundstück Flst. 2389/23"'
                style={formInputStyle()}
                autoFocus
              />
            </div>
            <div>
              <label style={formLabelStyle()}>Objekttyp</label>
              <select
                value={newObjTyp}
                onChange={(e) => setNewObjTyp(e.target.value)}
                style={formInputStyle()}
              >
                {OBJEKTTYP_OPTIONS.map(o => (
                  <option key={o.value} value={o.value}>{o.label}</option>
                ))}
              </select>
            </div>
            {addObjError && (
              <div style={{ color: 'var(--danger)', fontSize: 12 }}>{addObjError}</div>
            )}
            <div style={{ display: 'flex', gap: 'var(--space-2)', justifyContent: 'flex-end' }}>
              <button
                type="button"
                className="btn btn-ghost btn-sm"
                onClick={() => { setAddObjektOpen(false); setNewObjBezeichnung(''); setAddObjError(null); }}
                disabled={addObjSaving}
              >Abbrechen</button>
              <button
                type="button"
                className="btn btn-primary btn-sm"
                onClick={handleAddObjekt}
                disabled={!newObjBezeichnung.trim() || addObjSaving}
              >
                {addObjSaving ? 'Legt an …' : 'Objekt anlegen'}
              </button>
            </div>
          </div>
        </div>
      )}

      {deleteObjConfirm && (
        <ConfirmDialog
          title="Bewertungsobjekt löschen"
          message={<>
            <strong>{deleteObjConfirm.obj.bezeichnung}</strong> wird aus dem Gutachten entfernt.
            Alle diesem Objekt zugeordneten Dokumente bleiben im System, verlieren aber die Objekt-Zuordnung.
            {deleteObjConfirm.error && <div style={{ color: 'var(--danger)', marginTop: 8 }}>{deleteObjConfirm.error}</div>}
          </>}
          confirmLabel="Objekt löschen"
          danger
          busy={deleteObjConfirm.busy}
          onConfirm={executeDeleteObjekt}
          onCancel={() => setDeleteObjConfirm(null)}
        />
      )}

      {/* Bewertung (Gutachten-Eckdaten) */}
      <StammdatenSektion
        title="Bewertung"
        fields={[g.wertermittlungsstichtag, g.verkehrswert, g.wertermittlungsmethode]}
      >
        <ObjTableRow label="Wertermittlungsstichtag" sourceHint="Beschluss">
          <EditableField value={g.wertermittlungsstichtag} type="date" onSave={(v) => updateGutachten('wertermittlungsstichtag', v)} disabled={!canEditGutachten} />
        </ObjTableRow>
        <ObjTableRow label="Qualitätsstichtag" sourceHint="Beschluss">
          <EditableField value={g.qualitaetsstichtag} type="date" onSave={(v) => updateGutachten('qualitaetsstichtag', v)} disabled={!canEditGutachten} />
        </ObjTableRow>
        <ObjTableRow label="Verkehrswert (EUR)" sourceHint="Ergebnis">
          <EditableField value={g.verkehrswert} type="number" display={g.verkehrswert ? `€${Number(g.verkehrswert).toLocaleString('de-DE')}` : null} onSave={(v) => updateGutachten('verkehrswert', v)} disabled={!canEditGutachten} />
        </ObjTableRow>
        <ObjTableRow label="Wertermittlungsmethode" sourceHint="manuell">
          <select
            value={g.wertermittlungsmethode || ''}
            onChange={async (e) => {
              try { await updateGutachten('wertermittlungsmethode', e.target.value || null); }
              catch (err) { alert('Speichern fehlgeschlagen: ' + err.message); }
            }}
            disabled={!canEditGutachten}
            style={{
              padding: '6px 10px', fontSize: 14, border: '1px solid var(--border-light)',
              borderRadius: 'var(--radius-sm)', background: 'var(--surface)',
              color: g.wertermittlungsmethode ? 'var(--text-primary)' : 'var(--text-tertiary)',
              cursor: canEditGutachten ? 'pointer' : 'default', outline: 'none', width: '100%',
            }}
          >
            <option value="">—</option>
            <option value="Sachwertverfahren">Sachwertverfahren</option>
            <option value="Ertragswertverfahren">Ertragswertverfahren</option>
            <option value="Vergleichswertverfahren">Vergleichswertverfahren</option>
          </select>
        </ObjTableRow>
        {(g.wertermittlungsstichtag_2 || g.verkehrswert_2) && (<>
          <ObjTableRow label="Stichtag 2">
            <EditableField value={g.wertermittlungsstichtag_2} type="date" onSave={(v) => updateGutachten('wertermittlungsstichtag_2', v)} disabled={!canEditGutachten} />
          </ObjTableRow>
          <ObjTableRow label="Verkehrswert 2 (EUR)">
            <EditableField value={g.verkehrswert_2} type="number" display={g.verkehrswert_2 ? `€${Number(g.verkehrswert_2).toLocaleString('de-DE')}` : null} onSave={(v) => updateGutachten('verkehrswert_2', v)} disabled={!canEditGutachten} />
          </ObjTableRow>
        </>)}
        {(g.wertermittlungsstichtag_3 || g.verkehrswert_3) && (<>
          <ObjTableRow label="Stichtag 3">
            <EditableField value={g.wertermittlungsstichtag_3} type="date" onSave={(v) => updateGutachten('wertermittlungsstichtag_3', v)} disabled={!canEditGutachten} />
          </ObjTableRow>
          <ObjTableRow label="Verkehrswert 3 (EUR)">
            <EditableField value={g.verkehrswert_3} type="number" display={g.verkehrswert_3 ? `€${Number(g.verkehrswert_3).toLocaleString('de-DE')}` : null} onSave={(v) => updateGutachten('verkehrswert_3', v)} disabled={!canEditGutachten} />
          </ObjTableRow>
        </>)}
      </StammdatenSektion>

      {/* Objekt-Stammdaten — bei mehreren Objekten alle untereinander gerendert.
          Lokale Helfer (oh, updateObjekt, canEditObjekt, groesseNumber) werden
          pro Objekt im Closure neu definiert, damit jede Sektion mit ihrem
          eigenen `obj` arbeitet. */}
      {/* Nur das aktive Objekt rendern — der Wechsel erfolgt über den
          Umschalter oben (statt langem Herunterscrollen durch alle Objekte). */}
      {obj && [obj].map((currentObj) => {
        const obj = currentObj;
        const objIdx = aktivIdx;
        const oh = (feld) => obj?.id ? herkunft?.[`bewertungsobjekte:${obj.id}:${feld}`] : null;
        const canEditObjekt = canEdit && !!obj?.id;
        const updateObjekt = async (feld, wert) => {
          if (!obj?.id) throw new Error('Objekt-ID fehlt');
          const numericFields = new Set(['groesse_qm', 'wohnflaeche', 'baujahr', 'wertansatz', 'sort_order']);
          const normalized = numericFields.has(feld) && wert != null && wert !== ''
            ? Number(wert) : (wert == null || wert === '' ? null : wert);
          await apiPatchRow('bewertungsobjekte', obj.id, { [feld]: normalized }, session, workerUrl);
          onRefresh && onRefresh();
        };
        const groesseNumber = (() => {
          if (!obj?.groesse) return null;
          const m = /(\d[\d.,]*)/.exec(obj.groesse);
          return m ? Number(m[1].replace(/\./g, '').replace(',', '.')) : null;
        })();

        return (
      <div key={obj?.id || objIdx} id={`objekt-stammdaten-${objIdx}`} style={{ marginTop: 'var(--space-2)' }}>

        {/* Gebäude (inkl. Adresse) */}
        <StammdatenSektion title="Gebäude" openSignal={sektionSignal('Gebäude')} fields={[g.adresse, obj.bezeichnung, obj.objekttyp, obj.baujahr, obj.wohnflaeche]}>
          <ObjTableRow label="Adresse" id="feld-adresse" herkunft={gh('adresse')} onOpenInline={handleOpenInline} sourceHint="Beschluss">
            <EditableField
              value={g.adresse}
              onSave={(v) => updateGutachten('adresse', v)}
              disabled={!canEditGutachten}
              openSignal={jumpKey === 'adresse' ? jumpNonce : 0}
              onEnterNext={() => jumpToFeld(nextOpenFeld('adresse'))}
            />
          </ObjTableRow>
          <ObjTableRow label="Bezeichnung" id="feld-bezeichnung" sourceHint="manuell">
            <EditableField value={obj.bezeichnung} onSave={(v) => updateObjekt('bezeichnung', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'bezeichnung' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('bezeichnung'))} />
          </ObjTableRow>
          <ObjTableRow label="Objekttyp" id="feld-objekttyp" herkunft={oh('objekttyp')} onOpenInline={handleOpenInline} sourceHint="Beschluss / manuell">
            <EditableField value={OBJEKTTYP_OPTIONS.find(o => o.label === obj.objekttyp)?.value ?? ''} type="select" options={OBJEKTTYP_OPTIONS} display={obj.objekttyp} onSave={(v) => updateObjekt('objekttyp', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'objekttyp' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('objekttyp'))} />
          </ObjTableRow>
          <ObjTableRow label="Baujahr" id="feld-baujahr" herkunft={oh('baujahr')} onOpenInline={handleOpenInline} sourceHint="Energieausweis / manuell">
            <EditableField value={obj.baujahr} type="number" onSave={(v) => updateObjekt('baujahr', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'baujahr' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('baujahr'))} />
          </ObjTableRow>
          <ObjTableRow label="Wohn-/Nutzfläche (m²)" id="feld-wohnflaeche" herkunft={oh('wohnflaeche')} onOpenInline={handleOpenInline} sourceHint="Energieausweis / Ortstermin">
            <EditableField value={obj.wohnflaeche} type="number" onSave={(v) => updateObjekt('wohnflaeche', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'wohnflaeche' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('wohnflaeche'))} />
          </ObjTableRow>
        </StammdatenSektion>

        {/* Grundstück */}
        <StammdatenSektion title="Grundstück" openSignal={sektionSignal('Grundstück')} fields={[obj.gemarkung, obj.flur, obj.flurstueck, obj.groesse_qm, obj.grundbuchblatt, obj.wirtschaftsart]}>
          <ObjTableRow label="Gemarkung" id="feld-gemarkung" herkunft={oh('gemarkung')} onOpenInline={handleOpenInline} sourceHint="Beschluss / Grundbuch">
            <EditableField value={obj.gemarkung} onSave={(v) => updateObjekt('gemarkung', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'gemarkung' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('gemarkung'))} />
          </ObjTableRow>
          <ObjTableRow label="Flur" id="feld-flur" herkunft={oh('flur')} onOpenInline={handleOpenInline} sourceHint="Grundbuchauszug">
            <EditableField value={obj.flur} onSave={(v) => updateObjekt('flur', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'flur' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('flur'))} />
          </ObjTableRow>
          <ObjTableRow label="Flurstück" id="feld-flurstueck" herkunft={oh('flurstueck')} onOpenInline={handleOpenInline} sourceHint="Beschluss / Grundbuch">
            <EditableField value={obj.flurstueck} onSave={(v) => updateObjekt('flurstueck', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'flurstueck' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('flurstueck'))} />
          </ObjTableRow>
          <ObjTableRow label="Grundstücksgröße (m²)" id="feld-groesse_qm" herkunft={oh('groesse_qm')} onOpenInline={handleOpenInline} sourceHint="Beschluss / Grundbuch">
            <EditableField value={groesseNumber} type="number" display={obj.groesse} onSave={(v) => updateObjekt('groesse_qm', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'groesse_qm' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('groesse_qm'))} />
          </ObjTableRow>
          <ObjTableRow label="Grundbuchblatt" herkunft={oh('grundbuchblatt')} onOpenInline={handleOpenInline} sourceHint="Grundbuchauszug">
            <EditableField value={obj.grundbuchblatt} onSave={(v) => updateObjekt('grundbuchblatt', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Wirtschaftsart" herkunft={oh('wirtschaftsart')} onOpenInline={handleOpenInline} sourceHint="Grundbuchauszug">
            <EditableField value={obj.wirtschaftsart} onSave={(v) => updateObjekt('wirtschaftsart', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
        </StammdatenSektion>

        {/* Eigentum (WEG) */}
        <StammdatenSektion title="Eigentum (WEG)" fields={[obj.mea, obj.sondereigentumseinheit, obj.bruchteilseigentum, obj.abt_2_eintragungen]}>
          <ObjTableRow label="MEA" herkunft={oh('mea')} onOpenInline={handleOpenInline} sourceHint="Teilungserklärung / Grundbuch">
            <EditableField value={obj.mea} onSave={(v) => updateObjekt('mea', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Sondereigentumseinheit" herkunft={oh('sondereigentumseinheit')} onOpenInline={handleOpenInline} sourceHint="Teilungserklärung">
            <EditableField value={obj.sondereigentumseinheit} onSave={(v) => updateObjekt('sondereigentumseinheit', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Bruchteilseigentum" herkunft={oh('bruchteilseigentum')} onOpenInline={handleOpenInline} sourceHint="Grundbuchauszug">
            <EditableField value={obj.bruchteilseigentum} onSave={(v) => updateObjekt('bruchteilseigentum', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Eintragungen Abt. II" herkunft={oh('abt_2_eintragungen')} onOpenInline={handleOpenInline} sourceHint="Grundbuchauszug">
            <EditableField value={obj.abt_2_eintragungen} type="textarea" placeholder="— keine —" onSave={(v) => updateObjekt('abt_2_eintragungen', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
        </StammdatenSektion>

        {/* Planungsrecht */}
        <StammdatenSektion title="Planungsrecht" openSignal={sektionSignal('Planungsrecht')} fields={[obj.art_bauliche_nutzung, obj.grz, obj.gfz, obj.vollgeschosse_zulaessig, obj.bauweise, obj.bplan_nr]}>
          <ObjTableRow label="Art bauliche Nutzung" id="feld-art_bauliche_nutzung" herkunft={oh('art_bauliche_nutzung')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.art_bauliche_nutzung} onSave={(v) => updateObjekt('art_bauliche_nutzung', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'art_bauliche_nutzung' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('art_bauliche_nutzung'))} />
          </ObjTableRow>
          <ObjTableRow label="GRZ" id="feld-grz" herkunft={oh('grz')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.grz} type="number" onSave={(v) => updateObjekt('grz', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'grz' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('grz'))} />
          </ObjTableRow>
          <ObjTableRow label="GFZ" id="feld-gfz" herkunft={oh('gfz')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.gfz} type="number" onSave={(v) => updateObjekt('gfz', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'gfz' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('gfz'))} />
          </ObjTableRow>
          <ObjTableRow label="Zul. Vollgeschosse" herkunft={oh('vollgeschosse_zulaessig')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.vollgeschosse_zulaessig} onSave={(v) => updateObjekt('vollgeschosse_zulaessig', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Bauweise" herkunft={oh('bauweise')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.bauweise} onSave={(v) => updateObjekt('bauweise', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Dachform" herkunft={oh('dachform')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.dachform} onSave={(v) => updateObjekt('dachform', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="B-Plan Nr." herkunft={oh('bplan_nr')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.bplan_nr} onSave={(v) => updateObjekt('bplan_nr', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="B-Plan Status" herkunft={oh('bplan_status')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.bplan_status} onSave={(v) => updateObjekt('bplan_status', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Festsetzungen" herkunft={oh('festsetzungen_text')} onOpenInline={handleOpenInline} sourceHint="Bebauungsplan">
            <EditableField value={obj.festsetzungen_text} type="textarea" onSave={(v) => updateObjekt('festsetzungen_text', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
        </StammdatenSektion>

        {/* Bodenwert */}
        <StammdatenSektion title="Bodenwert" openSignal={sektionSignal('Bodenwert')} fields={[obj.bodenrichtwert, obj.brw_stichtag, obj.brw_zone, obj.brw_entwicklungszustand]}>
          <ObjTableRow label="Bodenrichtwert (EUR/m²)" id="feld-bodenrichtwert" herkunft={oh('bodenrichtwert')} onOpenInline={handleOpenInline} sourceHint="Bodenrichtwertkarte">
            <EditableField value={obj.bodenrichtwert} type="number" onSave={(v) => updateObjekt('bodenrichtwert', v)} disabled={!canEditObjekt} openSignal={jumpKey === 'bodenrichtwert' ? jumpNonce : 0} onEnterNext={() => jumpToFeld(nextOpenFeld('bodenrichtwert'))} />
          </ObjTableRow>
          <ObjTableRow label="BRW Stichtag" herkunft={oh('brw_stichtag')} onOpenInline={handleOpenInline} sourceHint="Bodenrichtwertkarte">
            <EditableField value={obj.brw_stichtag} type="date" onSave={(v) => updateObjekt('brw_stichtag', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Richtwertzone" herkunft={oh('brw_zone')} onOpenInline={handleOpenInline} sourceHint="Bodenrichtwertkarte">
            <EditableField value={obj.brw_zone} onSave={(v) => updateObjekt('brw_zone', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Entwicklungszustand" herkunft={oh('brw_entwicklungszustand')} onOpenInline={handleOpenInline} sourceHint="Bodenrichtwertkarte">
            <EditableField value={obj.brw_entwicklungszustand} onSave={(v) => updateObjekt('brw_entwicklungszustand', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="BRW Nutzungsart" herkunft={oh('brw_nutzungsart')} onOpenInline={handleOpenInline} sourceHint="Bodenrichtwertkarte">
            <EditableField value={obj.brw_nutzungsart} onSave={(v) => updateObjekt('brw_nutzungsart', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="BRW Bezugs-GFZ" herkunft={oh('brw_gfz')} onOpenInline={handleOpenInline} sourceHint="Bodenrichtwertkarte">
            <EditableField value={obj.brw_gfz} type="number" onSave={(v) => updateObjekt('brw_gfz', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
        </StammdatenSektion>

        {/* Energie */}
        <StammdatenSektion title="Energie" fields={[obj.endenergie, obj.effizienzklasse, obj.energietraeger, obj.baujahr_heizung]}>
          <ObjTableRow label="Endenergie (kWh/m²a)" herkunft={oh('endenergie')} onOpenInline={handleOpenInline} sourceHint="Energieausweis">
            <EditableField value={obj.endenergie} type="number" onSave={(v) => updateObjekt('endenergie', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Primärenergie (kWh/m²a)" herkunft={oh('primaerenergie')} onOpenInline={handleOpenInline} sourceHint="Energieausweis">
            <EditableField value={obj.primaerenergie} type="number" onSave={(v) => updateObjekt('primaerenergie', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Effizienzklasse" herkunft={oh('effizienzklasse')} onOpenInline={handleOpenInline} sourceHint="Energieausweis">
            <EditableField value={obj.effizienzklasse || ''} type="select" options={EFFIZIENZKLASSE_OPTIONS} display={obj.effizienzklasse} onSave={(v) => updateObjekt('effizienzklasse', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Ausweis-Typ" herkunft={oh('energieausweis_typ')} onOpenInline={handleOpenInline} sourceHint="Energieausweis">
            <EditableField value={obj.energieausweis_typ || ''} type="select" options={ENERGIEAUSWEIS_TYP_OPTIONS} display={obj.energieausweis_typ === 'bedarfsausweis' ? 'Bedarfsausweis' : obj.energieausweis_typ === 'verbrauchsausweis' ? 'Verbrauchsausweis' : obj.energieausweis_typ} onSave={(v) => updateObjekt('energieausweis_typ', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Energieträger" herkunft={oh('energietraeger')} onOpenInline={handleOpenInline} sourceHint="Energieausweis">
            <EditableField value={obj.energietraeger} onSave={(v) => updateObjekt('energietraeger', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
          <ObjTableRow label="Baujahr Heizung" herkunft={oh('baujahr_heizung')} onOpenInline={handleOpenInline} sourceHint="Energieausweis">
            <EditableField value={obj.baujahr_heizung} type="number" onSave={(v) => updateObjekt('baujahr_heizung', v)} disabled={!canEditObjekt} />
          </ObjTableRow>
        </StammdatenSektion>

        {/* Aus Dokumenten extrahierte Datenpunkte — objekt-bezogen + gutachten/auftrag-weit */}
        <ObjektDokumentDaten
          dokumente={relevanteDokumente.filter(d =>
            (d.scope === 'objekt' && d.objekt_id === obj?.id) ||
            d.scope === 'gutachten' || d.scope === 'auftrag'
          )}
          session={session}
          workerUrl={workerUrl}
          onOpenDokument={handleOpenInline}
          onRefresh={onRefresh}
        />
      </div>
        );
      })}
    </>
  );

  // Layout
  if (viewerActive) {
    const viewerPlaceholder = multiObj
      ? `Keine passenden Dokumente für ${obj?.bezeichnung || 'dieses Objekt'}. Grundbuch, Baupläne oder andere objektspezifische Unterlagen laden.`
      : 'Keine Gutachten- oder Objekt-Dokumente vorhanden. Grundbuch, Baupläne oder Teilungserklärung laden, um diese hier zu referenzieren.';
    const viewer = (
      <DokumentViewer
        dokumente={relevanteDokumente}
        session={session}
        workerUrl={workerUrl}
        activeId={viewerDokId}
        onChangeActive={setViewerDokId}
        pdfOnly
        expanded={pdfMax}
        onToggleExpanded={() => setPdfMax(m => !m)}
        placeholder={viewerPlaceholder}
      />
    );
    // Maximiert: Datenspalte ausblenden, PDF in voller Breite — zum genauen Lesen
    if (pdfMax) {
      return <div style={{ marginTop: 'var(--space-2)' }}>{viewer}</div>;
    }
    return (
      <div className="doc-split">
        <div>{dataColumn}</div>
        <div style={{ position: 'sticky', top: 'var(--space-4)' }}>{viewer}</div>
      </div>
    );
  }
  return <>{dataColumn}</>;
};

// Gebündeltes Anfordern offener Pflichtunterlagen: Auswahl (nach Kategorie
// gruppiert, alle vorausgewählt) + eine Frist → markiert alle gewählten als
// „angefragt" (bewährter /api/dokument/anfrage-Mechanismus, als Stapel).
// Die Brief-PDFs laufen weiter über das bestehende Bulk-Anschreiben (onLetters).
const UnterlagenAnfordernModal = ({ offeneListe, onClose, onConfirm, onLetters }) => {
  const [selected, setSelected] = useState(() => new Set((offeneListe || []).map(it => it.id)));
  const [frist, setFrist] = useState(() => new Date(Date.now() + 14 * 86400000).toISOString().split('T')[0]);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  const gruppen = useMemo(() => {
    const m = new Map();
    for (const it of (offeneListe || [])) {
      const k = it.category || 'Weitere';
      if (!m.has(k)) m.set(k, []);
      m.get(k).push(it);
    }
    return [...m.entries()];
  }, [offeneListe]);

  const toggle = (id) => setSelected(prev => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; });
  const count = selected.size;

  const confirm = async () => {
    if (count === 0) return;
    setSaving(true); setError(null);
    try {
      await onConfirm((offeneListe || []).filter(it => selected.has(it.id)), frist);
      onClose();
    } catch (e) {
      setError(e.message || String(e));
      setSaving(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={() => !saving && onClose()}>
      <div className="modal" style={{ maxWidth: 560 }} onClick={e => e.stopPropagation()}>
        <div className="modal-header">
          <span className="card-title">Unterlagen anfordern</span>
          <button className="modal-close" onClick={() => !saving && onClose()}>×</button>
        </div>
        <div className="modal-body">
          <p style={{ fontSize: 13, color: 'var(--text-secondary)', margin: '0 0 var(--space-3)', lineHeight: 1.5 }}>
            Wähle die noch offenen Unterlagen und setze eine Frist. Sie werden als <strong>angefragt</strong> markiert (mit Fristen-Tracking). Die Anschreiben als PDF erzeugst du anschließend gebündelt.
          </p>
          {gruppen.map(([cat, items]) => (
            <div key={cat} style={{ marginBottom: 'var(--space-3)' }}>
              <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-tertiary)', marginBottom: 6 }}>{cat}</div>
              {items.map(it => (
                <label key={it.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 8px', borderRadius: 'var(--radius-sm)', cursor: 'pointer', fontSize: 14 }}>
                  <input type="checkbox" checked={selected.has(it.id)} onChange={() => toggle(it.id)} style={{ width: 16, height: 16, accentColor: 'var(--vl-blue)' }} />
                  <span style={{ color: 'var(--text-primary)' }}>{it.label}</span>
                </label>
              ))}
            </div>
          ))}
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 'var(--space-3)', paddingTop: 'var(--space-3)', borderTop: '1px solid var(--border-light)' }}>
            <label style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)' }}>Frist bis</label>
            <input type="date" value={frist} onChange={e => setFrist(e.target.value)}
              style={{ padding: '6px 10px', fontSize: 13, border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)', color: 'var(--text-primary)', background: 'var(--surface)' }} />
          </div>
          {error && <div style={{ marginTop: 'var(--space-2)', color: 'var(--danger)', fontSize: 13 }}>Fehler: {error}</div>}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, padding: 'var(--space-4) var(--space-6)', borderTop: '1px solid var(--border-light)' }}>
          <button type="button" onClick={onLetters} style={{ background: 'none', border: 'none', color: 'var(--vl-blue)', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
            Anschreiben (PDF) erzeugen →
          </button>
          <div style={{ display: 'flex', gap: 'var(--space-3)' }}>
            <button type="button" onClick={() => !saving && onClose()} disabled={saving}
              style={{ border: '1px solid var(--border-light)', background: 'var(--surface)', color: 'var(--text-secondary)', fontSize: 13, fontWeight: 500, padding: '8px 16px', borderRadius: 'var(--radius-sm)', cursor: 'pointer' }}>
              Abbrechen
            </button>
            <button type="button" onClick={confirm} disabled={saving || count === 0}
              style={{ border: 'none', background: count === 0 ? 'var(--border-medium)' : 'var(--vl-blue)', color: '#fff', fontSize: 13, fontWeight: 600, padding: '8px 16px', borderRadius: 'var(--radius-sm)', cursor: count === 0 ? 'default' : 'pointer' }}>
              {saving ? 'Markiere…' : `${count} als angefragt markieren`}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

const UnterlagenTab = ({ p, g, session, workerUrl, onRefresh, onOpenUpload, onOpenBulkUpload }) => {
  // Registry aus Worker (Fallback: statisches UPLOAD_TYPES)
  const registryTypes = useUploadTypes();
  const effectiveTypes = registryTypes || UPLOAD_TYPES;

  // Inline-Viewer: Klick auf Dokument-Row öffnet es rechts als Split-Screen
  const [viewerDokId, setViewerDokId] = useState(null);
  const viewerActive = !!viewerDokId;

  // Dokument-Delete-State
  const [deleteDocConfirm, setDeleteDocConfirm] = useState(null);

  // Anfrage-Dialog State (Fristen-Tracking + Anschreiben-Vorschau)
  const [anfrageDialog, setAnfrageDialog] = useState(null);

  // Bulk-Anschreiben-Modal (mehrere Anschreiben auf einmal als ZIP)
  const [showBulkAnschreiben, setShowBulkAnschreiben] = useState(false);

  // Gebündeltes „Unterlagen anfordern" (offene Pflichtunterlagen → angefragt)
  const [anfordernOpen, setAnfordernOpen] = useState(false);

  // ── A1: Manuelle Notiz zu den Unterlagen ──
  // Reiner Reminder im Tool. KEINE Generierungs-/Export-Anbindung — der Gutachter
  // pflegt den Inhalt bei Bedarf selbst ins Gutachten ein. Ausgefüllt → roter Rahmen.
  const [hinweisText, setHinweisText] = useState(g.unterlagen_hinweis || '');
  const [hinweisSaving, setHinweisSaving] = useState(false);
  const [hinweisGespeichert, setHinweisGespeichert] = useState(false);
  // Bei Gutachten-Wechsel den lokalen Text mit dem geladenen Wert synchronisieren
  useEffect(() => { setHinweisText(g.unterlagen_hinweis || ''); }, [g.id, g.unterlagen_hinweis]);
  const saveHinweis = useCallback(async () => {
    const neu = hinweisText.trim();
    if (neu === (g.unterlagen_hinweis || '')) return; // nichts geändert
    if (!g.id) return;
    setHinweisSaving(true);
    try {
      await apiPatchRow('gutachten', g.id, { unterlagen_hinweis: neu || null }, session, workerUrl);
      setHinweisGespeichert(true);
      setTimeout(() => setHinweisGespeichert(false), 2000);
      onRefresh && onRefresh();
    } catch (e) {
      console.error('Unterlagen-Hinweis speichern fehlgeschlagen', e);
    } finally {
      setHinweisSaving(false);
    }
  }, [hinweisText, g.unterlagen_hinweis, g.id, session, workerUrl, onRefresh]);

  // Akkordeon: welche Kategorien sind aufgeklappt — alle default zugeklappt für Überblick
  const [openCats, setOpenCats] = useState(() => new Set());

  const toggleCat = useCallback((cat) => {
    setOpenCats(prev => {
      const next = new Set(prev);
      if (next.has(cat)) next.delete(cat);
      else next.add(cat);
      return next;
    });
  }, []);

  // Segment-Filter für die Dokumentenliste (Alle/Vorhanden/Angefragt/Offen)
  const [docFilter, setDocFilter] = useState('alle');

  // Drag-&-Drop: Kategorie, über der gerade eine Datei schwebt (für Hervorhebung)
  const [dragOverCat, setDragOverCat] = useState(null);

  const openDokument = useCallback(async (dok) => {
    if (window.innerWidth < 768) {
      try {
        const res = await fetch(
          `${workerUrl}/api/document-url?id=${encodeURIComponent(dok.id)}`,
          { headers: { Authorization: `Bearer ${session.access_token}` } }
        );
        if (res.ok) { const { url } = await res.json(); window.open(url, '_blank'); return; }
      } catch {}
    }
    setViewerDokId(dok.id);
  }, [session, workerUrl]);

  const executeDeleteDokument = async () => {
    if (!deleteDocConfirm) return;
    setDeleteDocConfirm(s => ({ ...s, busy: true, error: null }));
    try {
      await apiDeleteDokument(deleteDocConfirm.dok.id, session, workerUrl);
      if (viewerDokId === deleteDocConfirm.dok.id) setViewerDokId(null);
      setDeleteDocConfirm(null);
      onRefresh && onRefresh();
    } catch (err) {
      setDeleteDocConfirm(s => ({ ...s, busy: false, error: err.message || String(err) }));
    }
  };

  // Alle Bewertungsobjekte (für Objekt-Zuordnung)
  const allObjekte = useMemo(() => {
    return (p.gutachten || []).flatMap(g => (g.objekte || []).map(o => ({
      ...o,
      gutachten_id: g.id,
      gutachtenTitel: g.titel || g.adresse || 'Gutachten',
    })));
  }, [p.gutachten]);

  // Scope-Lookup pro UPLOAD_TYPES-Gruppe → bestimmt Sektionszugehörigkeit
  const auftragScopeGroups = useMemo(() => {
    const set = new Set();
    for (const group of effectiveTypes) {
      if (group.scope === 'auftrag' || group.scope === 'flex') {
        set.add(group.group);
        for (const item of group.items) set.add(item.category || group.group);
      }
    }
    return set;
  }, [effectiveTypes]);

  // Dokumente nach Kategorie gruppieren, getrennt nach Auftrag/Objekt
  const { auftragDocsByCat, objektDocsByCat } = useMemo(() => {
    const auftrag = {};
    const objekt = {};
    for (const cat of UNTERLAGEN_CATEGORIES) { auftrag[cat] = []; objekt[cat] = []; }
    for (const d of (p.dokumente || [])) {
      const cat = getCategoryForType(d.typ_raw, effectiveTypes) || 'Sonstiges';
      const isAuftragDoc = d.scope === 'auftrag' || auftragScopeGroups.has(cat);
      const target = isAuftragDoc ? auftrag : objekt;
      if (!target[cat]) target[cat] = [];
      target[cat].push(d);
    }
    return { auftragDocsByCat: auftrag, objektDocsByCat: objekt };
  }, [p.dokumente, effectiveTypes, auftragScopeGroups]);

  // Upload-Typen pro Kategorie + Sektion
  const { auftragTypesByCat, objektTypesByCat } = useMemo(() => {
    const auftrag = {};
    const objekt = {};
    for (const group of effectiveTypes) {
      const isAuftragGroup = group.scope === 'auftrag' || group.scope === 'flex';
      for (const item of group.items) {
        const cat = item.category || group.group || 'Sonstiges';
        const target = isAuftragGroup ? auftrag : objekt;
        if (!target[cat]) target[cat] = [];
        target[cat].push(item);
      }
    }
    return { auftragTypesByCat: auftrag, objektTypesByCat: objekt };
  }, [effectiveTypes]);

  const auftragDocCount = useMemo(() =>
    Object.values(auftragDocsByCat).reduce((s, arr) => s + arr.length, 0),
    [auftragDocsByCat]
  );
  const objektDocCount = useMemo(() =>
    Object.values(objektDocsByCat).reduce((s, arr) => s + arr.length, 0),
    [objektDocsByCat]
  );
  const totalDocs = (p.dokumente || []).length;

  // Alle Dokumente für den Viewer
  const alleDokumenteFuerViewer = useMemo(() => {
    return (p.dokumente || []);
  }, [p.dokumente]);

  // Objekt-Zuordnung ändern
  const handleObjektAssign = async (dokId, objektId) => {
    try {
      await apiPatchRow('dokumente', dokId, {
        objekt_id: objektId || null,
        scope: objektId ? 'objekt' : 'gutachten',
      }, session, workerUrl);
      onRefresh && onRefresh();
    } catch (e) {
      console.error('[ObjektAssign]', e);
      alert('Zuordnung fehlgeschlagen: ' + (e.message || e));
    }
  };

  // ── Fristen-Tracking: Dokument anfragen ──
  const handleAnfrage = useCallback((typId, typLabel, scope) => {
    const gutachtenId = (p.gutachten || [])[0]?.id || null;
    setAnfrageDialog({
      typId, typLabel, scope: scope || 'gutachten',
      gutachtenId,
      fristBis: '',
      saving: false,
    });
  }, [p.gutachten]);

  const submitAnfrage = useCallback(async () => {
    if (!anfrageDialog) return;
    setAnfrageDialog(s => ({ ...s, saving: true }));
    try {
      await fetch(`${workerUrl}/api/dokument/anfrage`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${session.access_token}`,
        },
        body: JSON.stringify({
          project_id: p.id,
          typ: anfrageDialog.typId,
          scope: anfrageDialog.scope,
          gutachten_id: anfrageDialog.gutachtenId,
          frist_bis: anfrageDialog.fristBis || null,
        }),
      });
      setAnfrageDialog(null);
      onRefresh && onRefresh();
    } catch (err) {
      setAnfrageDialog(s => ({ ...s, saving: false }));
      alert('Fehler: ' + (err.message || err));
    }
  }, [anfrageDialog, p.id, session, workerUrl, onRefresh]);

  // Stapel: alle gewählten offenen Unterlagen als „angefragt" markieren (eine Frist)
  const bulkMarkAngefragt = useCallback(async (items, fristBis) => {
    const gutachtenId = (p.gutachten || [])[0]?.id || null;
    await Promise.all((items || []).map(it => fetch(`${workerUrl}/api/dokument/anfrage`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
      body: JSON.stringify({ project_id: p.id, typ: it.id, scope: it.scope || 'gutachten', gutachten_id: gutachtenId, frist_bis: fristBis || null }),
    }).then(r => { if (!r.ok) throw new Error('Anfrage fehlgeschlagen: ' + (it.label || it.id)); })));
    onRefresh && onRefresh();
  }, [p.id, p.gutachten, session, workerUrl, onRefresh]);

  // ── Fristen-Tracking: Eingetroffen markieren → Upload-Dialog öffnen ──
  const handleMarkEingetroffen = useCallback((dok) => {
    // Schritt 1: eingetroffen_am setzen
    apiPatchRow('dokumente', dok.id, {
      eingetroffen_am: new Date().toISOString().split('T')[0],
    }, session, workerUrl).then(() => {
      // Schritt 2: Upload-Dialog für diesen Typ öffnen
      onOpenUpload(dok.typ_raw);
      onRefresh && onRefresh();
    }).catch(err => {
      alert('Fehler: ' + (err.message || err));
    });
  }, [session, workerUrl, onOpenUpload, onRefresh]);

  const renderDokRow = (d, showObjektAssign = false) => {
    const aktiv = viewerDokId === d.id;
    const assignedObj = allObjekte.find(o => o.id === d.objekt_id);
    const fristStatus = getDokFristStatus(d);
    const isAnfrage = d.status === 'angefragt';

    return (
      <div
        key={d.id}
        className="doc-row"
        style={{
          cursor: isAnfrage ? 'default' : 'pointer',
          background: aktiv ? 'var(--surface-light)' : undefined,
          borderLeft: aktiv ? '3px solid var(--vl-orange)'
                    : isAnfrage ? '3px dashed var(--vl-blue-light, #6B8DB5)'
                    : '3px solid transparent',
          opacity: isAnfrage ? 0.85 : 1,
        }}
        onClick={() => !isAnfrage && openDokument(d)}
        title={isAnfrage ? 'Dokument angefragt, noch nicht hochgeladen' : 'Klicken zum Anzeigen'}
      >
        <div className="doc-icon">
          {isAnfrage ? <IconClock size={20} stroke="var(--vl-blue)" /> : <IconDocument size={20} />}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="doc-name" style={{ color: isAnfrage ? 'var(--text-secondary)' : undefined }}>
            {d.label || '(ohne Titel)'}
          </div>
          <div className="doc-meta">
            {d.typ || 'Dokument'}
            {d.angefragt_am && !d.eingetroffen_am && (
              <span style={{ marginLeft: 6, fontSize: 10, color: 'var(--text-tertiary)' }}>
                angefragt {formatDate(d.angefragt_am)}
              </span>
            )}
          </div>
        </div>

        {/* Fristen-Badge */}
        {fristStatus && (
          <span style={{
            fontSize: 10, fontWeight: 600,
            padding: '2px 8px', borderRadius: 10,
            background: fristStatus.bg,
            color: fristStatus.color,
            whiteSpace: 'nowrap', flexShrink: 0,
          }}>
            {fristStatus.label}
          </span>
        )}

        {/* Objekt-Zuordnung (nur bei hochgeladenen Docs) */}
        {!isAnfrage && showObjektAssign && allObjekte.length > 1 && (
          <select
            value={d.objekt_id || ''}
            onClick={(e) => e.stopPropagation()}
            onChange={(e) => { e.stopPropagation(); handleObjektAssign(d.id, e.target.value || null); }}
            style={{
              padding: '3px 20px 3px 6px',
              fontSize: 11, fontWeight: 500,
              border: `1px solid ${d.objekt_id ? 'var(--vl-blue-light, #6B8DB5)' : 'var(--border-light)'}`,
              borderRadius: 'var(--radius-sm)',
              background: d.objekt_id ? 'var(--surface-blue, #EEF3F8)' : 'var(--surface)',
              color: d.objekt_id ? 'var(--vl-blue)' : 'var(--text-tertiary)',
              cursor: 'pointer', outline: 'none',
              appearance: 'none',
              backgroundImage: `url("data:image/svg+xml,%3Csvg width='8' height='5' viewBox='0 0 8 5' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%23999' stroke-width='1.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")`,
              backgroundRepeat: 'no-repeat',
              backgroundPosition: 'right 6px center',
              maxWidth: 140,
              overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
            }}
            title={assignedObj ? `Zugeordnet: ${assignedObj.bezeichnung || assignedObj.objekttyp}` : 'Keinem Objekt zugeordnet'}
          >
            <option value="">— Objekt —</option>
            {allObjekte.map(o => (
              <option key={o.id} value={o.id}>
                {o.bezeichnung || o.objekttyp || 'Objekt'}
              </option>
            ))}
          </select>
        )}
        {/* Objekt-Badge (nur 1 Objekt vorhanden → kein Select nötig) */}
        {!isAnfrage && showObjektAssign && allObjekte.length === 1 && d.objekt_id && (
          <span style={{
            fontSize: 10, fontWeight: 600,
            padding: '2px 7px', borderRadius: 4,
            background: 'var(--surface-blue, #EEF3F8)',
            color: 'var(--vl-blue)',
            whiteSpace: 'nowrap',
          }}>{allObjekte[0].bezeichnung || allObjekte[0].objekttyp || 'Objekt'}</span>
        )}

        {/* Eingetroffen-Button für Anfragen */}
        {isAnfrage && (
          <button
            type="button"
            onClick={(e) => { e.stopPropagation(); handleMarkEingetroffen(d); }}
            title="Als eingetroffen markieren und Datei hochladen"
            style={{
              padding: '3px 10px', fontSize: 11, fontWeight: 600,
              background: 'var(--vl-blue)', color: '#fff',
              border: 'none', borderRadius: 'var(--radius-sm)',
              cursor: 'pointer', whiteSpace: 'nowrap', flexShrink: 0,
            }}
          >
            Hochladen
          </button>
        )}

        {!isAnfrage && <span className="doc-status doc-status-done">✓</span>}

        {session && (
          <button
            type="button"
            onClick={(e) => {
              e.stopPropagation();
              setDeleteDocConfirm({ dok: d, busy: false, error: null });
            }}
            title="Dokument löschen"
            style={{
              background: 'none', border: 'none',
              color: 'var(--text-tertiary)', cursor: 'pointer',
              padding: '2px 6px', fontSize: 16, lineHeight: 1,
              borderRadius: 'var(--radius-sm)',
            }}
            onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--danger)'; }}
            onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--text-tertiary)'; }}
          >×</button>
        )}
        {!isAnfrage && <IconChevronRight size={16} stroke="var(--text-tertiary)" />}
      </div>
    );
  };

  // Wiederverwendbare Kategorie-Akkordeon-Zeile
  const renderCategoryAccordion = (cat, docs, catTypes, showObjektAssign) => {
    let viewDocs = docs, viewTypes = catTypes;
    if (docFilter === 'vorhanden') { viewDocs = docs.filter(istDokVorhanden); viewTypes = []; }
    else if (docFilter === 'angefragt') { viewDocs = docs.filter(istDokOffeneAnfrage); viewTypes = []; }
    else if (docFilter === 'offen') {
      viewDocs = [];
      viewTypes = catTypes.filter(t => unterlagenStatus.requiredIds.has(t.id) && !unterlagenStatus.presentIds.has(t.id) && !unterlagenStatus.requestedIds.has(t.id));
    }
    const hasContent = viewDocs.length > 0 || viewTypes.length > 0;
    if (!hasContent) return null;
    const isOpen = docFilter !== 'alle' ? true : openCats.has(cat);
    const stat = unterlagenStatus.byCat[cat];
    const covColor = stat ? (stat.vorhanden >= stat.required ? 'var(--success)' : stat.vorhanden > 0 ? 'var(--warning)' : 'var(--text-tertiary)') : 'var(--text-tertiary)';
    // Drag-&-Drop: wahrscheinlichster Typ dieser Kategorie (Pflichttyp bevorzugt)
    const primaryType = catTypes.find(t => unterlagenStatus.requiredIds.has(t.id)) || catTypes[0];
    const isDragOver = dragOverCat === cat;

    return (
      <div
        key={cat}
        onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; if (dragOverCat !== cat) setDragOverCat(cat); }}
        onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOverCat(null); }}
        onDrop={(e) => {
          e.preventDefault();
          const f = e.dataTransfer?.files?.[0];
          setDragOverCat(null);
          if (f) onOpenUpload({ file: f, typ: primaryType?.id, noAutoStart: true });
        }}
        style={{
          borderBottom: '1px solid var(--border-light)',
          position: 'relative',
          borderRadius: isDragOver ? 'var(--radius-md)' : 0,
          outline: isDragOver ? '2px dashed var(--vl-blue)' : 'none',
          outlineOffset: -2,
          background: isDragOver ? 'rgba(0, 59, 113, 0.06)' : 'transparent',
          transition: 'background var(--dur-fast) var(--ease-out)',
        }}
      >
        {isDragOver && (
          <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', zIndex: 2 }}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: 'var(--vl-blue)', color: '#fff', fontSize: 13, fontWeight: 600, padding: '6px 14px', borderRadius: 'var(--radius-full)', boxShadow: 'var(--shadow-md)' }}>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3v12" /><path d="m7 10 5 5 5-5" /><path d="M5 21h14" /></svg>
              {primaryType ? `Als „${primaryType.label}" ablegen` : 'Datei hier ablegen'}
            </span>
          </div>
        )}
        <button
          type="button"
          onClick={() => toggleCat(cat)}
          style={{
            display: 'flex', alignItems: 'center', gap: 10,
            width: '100%', padding: '12px 16px',
            background: 'none', border: 'none',
            cursor: 'pointer', textAlign: 'left',
            fontSize: 13, fontWeight: 600,
            color: docs.length > 0 ? 'var(--text-primary)' : 'var(--text-secondary)',
            transition: 'background 0.12s',
          }}
          onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--surface-light)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = 'none'; }}
        >
          <span style={{
            fontSize: 14, color: 'var(--text-tertiary)',
            transition: 'transform 0.2s',
            transform: isOpen ? 'rotate(90deg)' : 'rotate(0deg)',
            display: 'inline-block', flexShrink: 0,
          }}>▸</span>
          <span style={{ flex: 1 }}>{cat}</span>
          {stat && stat.required > 0 ? (
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, fontWeight: 700, color: covColor }}>
              <span style={{ width: 8, height: 8, borderRadius: '50%', background: covColor, flexShrink: 0 }} />
              {stat.vorhanden}/{stat.required}
            </span>
          ) : docs.length > 0 ? (
            <span style={{ fontSize: 11, fontWeight: 700, background: 'var(--surface-light)', color: 'var(--text-secondary)', padding: '2px 8px', borderRadius: 10, minWidth: 22, textAlign: 'center' }}>{docs.length}</span>
          ) : null}
        </button>
        {isOpen && (
          <div style={{ paddingBottom: 8 }}>
            {viewDocs.length > 0 && (
              <div className="doc-list">
                {viewDocs.map(d => renderDokRow(d, showObjektAssign))}
              </div>
            )}
            {viewTypes.length > 0 && (
              <div style={{ padding: '8px 16px', display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                {viewTypes.map(item => {
                  const hasAnschreiben = ANSCHREIBEN_DOC_TYPES.has(item.id);
                  // Scope aus der UPLOAD_TYPES-Gruppe ermitteln
                  const parentGroup = effectiveTypes.find(g => g.items.some(i => i.id === item.id));
                  const itemScope = parentGroup?.scope || 'gutachten';
                  return (
                    <React.Fragment key={item.id}>
                      <button
                        type="button"
                        onClick={() => onOpenUpload(item.id)}
                        title={`${item.label} hochladen`}
                        style={{
                          padding: '5px 12px',
                          background: 'transparent',
                          border: '1px dashed var(--border-light)',
                          fontSize: 12, borderRadius: 'var(--radius-sm)',
                          color: 'var(--text-secondary)',
                          cursor: 'pointer', transition: 'all 0.12s',
                        }}
                        onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--vl-blue-light)'; e.currentTarget.style.color = 'var(--vl-blue)'; }}
                        onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--border-light)'; e.currentTarget.style.color = 'var(--text-secondary)'; }}
                      >
                        + {item.label}
                      </button>
                      {hasAnschreiben && (
                        <button
                          type="button"
                          onClick={() => handleAnfrage(item.id, item.label, itemScope)}
                          title={`${item.label} bei Behörde anfragen`}
                          style={{
                            padding: '5px 10px',
                            background: 'rgba(10,37,64,0.03)',
                            border: '1px dashed var(--vl-blue-light, #6B8DB5)',
                            fontSize: 11, borderRadius: 'var(--radius-sm)',
                            color: 'var(--vl-blue)',
                            cursor: 'pointer', transition: 'all 0.12s',
                          }}
                          onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(10,37,64,0.08)'; }}
                          onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(10,37,64,0.03)'; }}
                        >
                          Anfragen
                        </button>
                      )}
                    </React.Fragment>
                  );
                })}
              </div>
            )}
          </div>
        )}
      </div>
    );
  };

  // ── Unterlagen-Überblick: Pflicht-Soll (isTypeRequired) gegen Bestand ──
  // requiredFor = Auftragsart, requiredIf = Objekttyp. Buckets: vorhanden (Datei
  // da) · angefragt (offene Anfrage) · offen (benötigt, weder da noch angefragt).
  // Rein im Frontend — die Registry liefert required* bereits mit.
  const unterlagenStatus = useMemo(() => {
    const auftragsart = p.auftragsart_raw;
    const objekttypen = [...new Set(allObjekte.map(o => normObjekttyp(o.objekttyp)).filter(Boolean))];
    const flat = (effectiveTypes || []).flatMap(grp => (grp.items || []).map(it => ({
      id: it.id, label: it.label, requiredFor: it.requiredFor, requiredIf: it.requiredIf,
      scope: it.scope || grp.scope, category: it.category || grp.group,
    })));
    const seen = new Set();
    const required = [];
    for (const it of flat) {
      if (seen.has(it.id)) continue;
      seen.add(it.id);
      if (isTypeRequired(it, auftragsart, objekttypen)) required.push(it);
    }
    const docsByType = {};
    for (const d of (p.dokumente || [])) (docsByType[d.typ_raw] = docsByType[d.typ_raw] || []).push(d);
    const requiredIds = new Set(), presentIds = new Set(), requestedIds = new Set();
    const byCat = {};
    let vorhanden = 0, angefragt = 0, offen = 0, ueberfaellig = 0;
    const offeneListe = [];
    for (const it of required) {
      requiredIds.add(it.id);
      const docs = docsByType[it.id] || [];
      const cat = it.category || 'Sonstiges';
      const bc = byCat[cat] || (byCat[cat] = { required: 0, vorhanden: 0, angefragt: 0, offen: 0 });
      bc.required++;
      if (docs.some(istDokVorhanden)) {
        vorhanden++; bc.vorhanden++; presentIds.add(it.id);
      } else {
        const reqDocs = docs.filter(istDokOffeneAnfrage);
        if (reqDocs.length) {
          angefragt++; bc.angefragt++; requestedIds.add(it.id);
          if (reqDocs.some(d => { const st = getDokFristStatus(d); return st && st.key === 'ueberfaellig'; })) ueberfaellig++;
        } else {
          offen++; bc.offen++; offeneListe.push(it);
        }
      }
    }
    const vorhandenDocs = (p.dokumente || []).filter(istDokVorhanden).length;
    const angefragtDocs = (p.dokumente || []).filter(istDokOffeneAnfrage).length;
    return { required: required.length, vorhanden, angefragt, offen, ueberfaellig, offeneListe, byCat, requiredIds, presentIds, requestedIds, vorhandenDocs, angefragtDocs };
  }, [effectiveTypes, p.dokumente, p.auftragsart_raw, allObjekte]);

  const dataColumn = (
    <>
      {/* ── Unterlagen-Überblick (Soll/Ist auf einen Blick) ── */}
      {(() => {
        const s = unterlagenStatus;
        const pct = s.required > 0 ? s.vorhanden / s.required : 0;
        const ringColor = pct >= 1 ? 'var(--success)' : pct > 0 ? 'var(--warning)' : 'var(--text-tertiary)';
        const RR = 26, CC = 2 * Math.PI * RR;
        const chip = (dot, label, n, bg) => (
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, padding: '5px 12px', borderRadius: 'var(--radius-full)', background: bg, fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)' }}>
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: dot, flexShrink: 0 }} />
            <span style={{ color: 'var(--text-primary)' }}>{n}</span> {label}
          </span>
        );
        return (
          <div className="card" style={{ marginBottom: 'var(--space-4)', padding: 'var(--space-5)' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-5)', flexWrap: 'wrap' }}>
              <div style={{ position: 'relative', width: 64, height: 64, flexShrink: 0 }}>
                <svg width="64" height="64" viewBox="0 0 64 64" style={{ transform: 'rotate(-90deg)' }}>
                  <circle cx="32" cy="32" r={RR} fill="none" stroke="var(--border-light)" strokeWidth="6" />
                  {pct > 0 && (
                    <circle cx="32" cy="32" r={RR} fill="none" stroke={ringColor} strokeWidth="6" strokeLinecap="round"
                      strokeDasharray={CC} strokeDashoffset={CC * (1 - pct)}
                      style={{ transition: 'stroke-dashoffset var(--dur-slow) var(--ease-out), stroke var(--dur-base)' }} />
                  )}
                </svg>
                <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 15, fontWeight: 700, color: 'var(--text-primary)' }}>
                  {Math.round(pct * 100)}%
                </div>
              </div>
              <div style={{ flex: 1, minWidth: 220 }}>
                <div style={{ fontSize: 17, fontWeight: 700, color: 'var(--text-primary)', letterSpacing: '-0.01em' }}>Unterlagen-Überblick</div>
                <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 }}>
                  {s.required > 0 ? `${s.vorhanden} von ${s.required} benötigten Unterlagen vorhanden` : 'Keine Pflichtunterlagen ermittelt — Auftrags- bzw. Objektart prüfen'}
                </div>
                <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 'var(--space-3)' }}>
                  {chip('var(--success)', 'vorhanden', s.vorhanden, 'var(--success-bg)')}
                  {chip('var(--vl-blue)', 'angefragt', s.angefragt, 'rgba(10,37,64,0.06)')}
                  {chip(s.offen > 0 ? 'var(--warning)' : 'var(--text-tertiary)', 'offen', s.offen, s.offen > 0 ? 'var(--warning-bg)' : 'var(--surface-light)')}
                  {s.ueberfaellig > 0 && (
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 12px', borderRadius: 'var(--radius-full)', background: 'var(--danger-bg, rgba(220,38,38,0.08))', color: 'var(--danger)', fontSize: 13, fontWeight: 600 }}>
                      {s.ueberfaellig} überfällig
                    </span>
                  )}
                </div>
              </div>
            </div>
            {s.offeneListe.length > 0 && (
              <div style={{ marginTop: 'var(--space-4)', paddingTop: 'var(--space-3)', borderTop: '1px solid var(--border-light)' }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 'var(--space-2)' }}>
                  <span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-tertiary)' }}>Noch offen</span>
                  <button type="button" onClick={() => setAnfordernOpen(true)}
                    style={{ border: 'none', background: 'var(--vl-blue)', color: 'var(--text-inverse, #fff)', fontSize: 12, fontWeight: 600, padding: '5px 12px', borderRadius: 'var(--radius-full)', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 2 11 13" /><path d="M22 2 15 22l-4-9-9-4 20-7z" /></svg>
                    Offene anfordern
                  </button>
                </div>
                <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                  {s.offeneListe.slice(0, 10).map(it => {
                    const canRequest = ANSCHREIBEN_DOC_TYPES.has(it.id);
                    return (
                      <span key={it.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: canRequest ? '4px 5px 4px 12px' : '6px 12px', borderRadius: 'var(--radius-full)', background: 'var(--surface-light)', border: '1px solid var(--border-light)', fontSize: 13 }}>
                        <span style={{ color: 'var(--text-secondary)' }}>{it.label}</span>
                        {canRequest && (
                          <button type="button" onClick={() => handleAnfrage(it.id, it.label, it.scope)}
                            style={{ border: 'none', background: 'var(--vl-blue)', color: 'var(--text-inverse, #fff)', fontSize: 12, fontWeight: 600, padding: '3px 10px', borderRadius: 'var(--radius-full)', cursor: 'pointer' }}>
                            Anfragen
                          </button>
                        )}
                      </span>
                    );
                  })}
                  {s.offeneListe.length > 10 && (
                    <span style={{ fontSize: 12, color: 'var(--text-tertiary)', alignSelf: 'center' }}>+{s.offeneListe.length - 10} weitere</span>
                  )}
                </div>
              </div>
            )}
          </div>
        );
      })()}
      {/* ── A1: Manuelle Notiz zu den Unterlagen (Reminder, keine Export-Anbindung) ── */}
      <div className="card" style={{
        marginBottom: 'var(--space-4)',
        border: hinweisText.trim() ? '2px solid #DC2626' : undefined,
      }}>
        <div className="card-header">
          <span className="card-title" style={{ color: hinweisText.trim() ? '#DC2626' : undefined }}>
            Manuelle Notiz zu den Unterlagen
          </span>
          {hinweisGespeichert && (
            <span style={{ fontSize: 12, color: 'var(--success, #16A34A)', fontWeight: 600 }}>
              ✓ Gespeichert
            </span>
          )}
        </div>
        <div style={{ padding: 'var(--space-3) var(--space-4) var(--space-4)' }}>
          <p style={{ fontSize: 12, color: 'var(--text-secondary)', margin: '0 0 var(--space-3)', lineHeight: 1.5 }}>
            Hier können Sie Unterlagen oder Hinweise notieren, die nicht als Dokument hochgeladen sind
            (z.&nbsp;B. telefonische Auskünfte, Einsichtnahmen vor Ort). Diese Notiz wird <strong>nicht</strong> automatisch
            in den Gutachtenentwurf übernommen — sie dient als Erinnerung, sie bei Bedarf manuell einzupflegen.
          </p>
          <textarea
            value={hinweisText}
            onChange={(e) => setHinweisText(e.target.value)}
            onBlur={saveHinweis}
            disabled={hinweisSaving || !g.id}
            rows={3}
            placeholder="z. B. Bebauungsplan am 12.05. im Bauamt eingesehen; telefonische Auskunft Hausverwaltung …"
            style={{
              width: '100%', boxSizing: 'border-box',
              padding: 'var(--space-2) var(--space-3)',
              fontSize: 13, lineHeight: 1.5,
              border: hinweisText.trim() ? '1.5px solid #DC2626' : '1px solid var(--border-light)',
              borderRadius: 'var(--radius-sm)',
              resize: 'vertical', fontFamily: 'inherit',
              color: 'var(--text-primary)', background: 'var(--surface)',
            }}
          />
          {hinweisSaving && (
            <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Speichert…</span>
          )}
        </div>
      </div>
      {/* ── Filter: Alle / Vorhanden / Angefragt / Offen (Glas-Segmented-Control) ── */}
      <div className="objekt-switcher" style={{ marginBottom: 'var(--space-4)' }}>
        {[['alle', 'Alle', null], ['vorhanden', 'Vorhanden', unterlagenStatus.vorhandenDocs], ['angefragt', 'Angefragt', unterlagenStatus.angefragtDocs], ['offen', 'Offen', unterlagenStatus.offen]].map(([key, label, count]) => (
          <button key={key} type="button" className={'objekt-chip' + (docFilter === key ? ' active' : '')} onClick={() => setDocFilter(key)}>
            {label}{count != null ? ` ${count}` : ''}
          </button>
        ))}
      </div>
      {/* ── Auftragsunterlagen (übergeordnet) ── */}
      <div className="card" style={{ marginBottom: 'var(--space-4)' }}>
        <div className="card-header">
          <span className="card-title">
            Auftragsunterlagen ({auftragDocCount})
          </span>
          <div style={{ display: 'flex', gap: 6 }}>
            <button className="btn btn-ghost btn-sm" onClick={() => setShowBulkAnschreiben(true)}
              style={{ fontSize: 12 }}
              title="Mehrere Behörden auf einmal anfragen — ein ZIP mit allen Schreiben">
              ✉️ Mehrere anfragen
            </button>
            <button className="btn btn-ghost btn-sm" onClick={onOpenBulkUpload}
              style={{ fontSize: 12 }}>
              Stapel hochladen
            </button>
            <button className="btn btn-accent btn-sm" onClick={onOpenUpload}>+ Hinzufügen</button>
          </div>
        </div>
        {auftragDocCount === 0 && (
          <div style={{
            padding: 'var(--space-4) var(--space-5)', fontSize: 13,
            color: 'var(--text-tertiary)',
          }}>
            Beschluss, Anschreiben und andere auftragsbezogene Dokumente hier ablegen.
          </div>
        )}
        <div>
          {UNTERLAGEN_CATEGORIES.map(cat => {
            if (!auftragScopeGroups.has(cat) && !(auftragDocsByCat[cat] || []).length) return null;
            const docs = auftragDocsByCat[cat] || [];
            const catTypes = auftragTypesByCat[cat] || [];
            if (docs.length === 0 && catTypes.length === 0) return null;
            return renderCategoryAccordion(cat, docs, catTypes, false);
          })}
        </div>
      </div>

      {/* ── Objektunterlagen (objektspezifisch) ── */}
      <div className="card" style={{ marginBottom: 'var(--space-5)' }}>
        <div className="card-header">
          <span className="card-title">
            Objektunterlagen ({objektDocCount})
          </span>
          {allObjekte.length > 1 && (
            <span style={{
              fontSize: 11, color: 'var(--text-tertiary)', fontWeight: 400,
            }}>
              {allObjekte.length} Objekte — per Dropdown zuordnen
            </span>
          )}
        </div>
        {objektDocCount === 0 && (
          <div style={{
            padding: 'var(--space-4) var(--space-5)', fontSize: 13,
            color: 'var(--text-tertiary)',
          }}>
            Grundbuch, Baupläne, Energieausweis und andere objektspezifische Unterlagen hier ablegen.
          </div>
        )}
        <div>
          {UNTERLAGEN_CATEGORIES.map(cat => {
            if (auftragScopeGroups.has(cat) && !(objektDocsByCat[cat] || []).length) return null;
            const docs = objektDocsByCat[cat] || [];
            const catTypes = objektTypesByCat[cat] || [];
            if (docs.length === 0 && catTypes.length === 0) return null;
            return renderCategoryAccordion(cat, docs, catTypes, true);
          })}
        </div>
      </div>

      {deleteDocConfirm && (
        <ConfirmDialog
          title="Dokument löschen"
          message={<>
            <strong>{deleteDocConfirm.dok.label || '(ohne Titel)'}</strong> wird unwiderruflich entfernt, inklusive der PDF-Datei und der Herkunfts-Verknüpfungen.
            <div style={{
              marginTop: 'var(--space-2)', padding: 'var(--space-2)',
              fontSize: 12, color: 'var(--text-secondary)',
              background: 'var(--surface-light)', borderRadius: 'var(--radius-sm)',
            }}>
              Aus diesem Dokument übernommene Feldwerte bleiben bestehen — sie zählen ab jetzt als manuell gesetzt.
            </div>
            {deleteDocConfirm.error && <div style={{ color: 'var(--danger)', marginTop: 8 }}>{deleteDocConfirm.error}</div>}
          </>}
          confirmLabel="Dokument löschen"
          danger
          busy={deleteDocConfirm.busy}
          onConfirm={executeDeleteDokument}
          onCancel={() => setDeleteDocConfirm(null)}
        />
      )}

      {/* ── Bulk-Anschreiben-Modal (mehrere Schreiben → ZIP) ── */}
      {anfordernOpen && (
        <UnterlagenAnfordernModal
          offeneListe={unterlagenStatus.offeneListe}
          onClose={() => setAnfordernOpen(false)}
          onConfirm={bulkMarkAngefragt}
          onLetters={() => { setAnfordernOpen(false); setShowBulkAnschreiben(true); }}
        />
      )}
      {showBulkAnschreiben && (
        <BulkAnschreibenModal
          auftragId={p.auftrag_id}
          projektGemeinde={p.gemeinde || (p.adresse || '').split(',').pop()?.trim().replace(/^\d{5}\s*/, '')}
          projektDokumente={p.dokumente || []}
          session={session}
          workerUrl={workerUrl}
          onClose={() => setShowBulkAnschreiben(false)}
        />
      )}

      {/* ── Anfrage-Dialog (Fristen-Tracking) ── */}
      {anfrageDialog && (
        <div className="modal-backdrop" onClick={() => !anfrageDialog.saving && setAnfrageDialog(null)}>
          <div className="modal" style={{ maxWidth: 480, width: '92vw' }} onClick={(e) => e.stopPropagation()}>
            <div className="modal-header">
              <div className="modal-title">{anfrageDialog.typLabel} anfragen</div>
              <button className="modal-close" onClick={() => setAnfrageDialog(null)}>×</button>
            </div>
            <div className="modal-body" style={{ padding: 'var(--space-4)' }}>
              <p style={{ fontSize: 13, color: 'var(--text-secondary)', margin: '0 0 var(--space-4) 0' }}>
                Das Dokument wird als <strong>angefragt</strong> im Unterlagen-Tab angezeigt. Sobald es eingetroffen ist, laden Sie die Datei hoch.
              </p>

              {/* Anschreiben herunterladen */}
              {ANSCHREIBEN_DOC_TYPES.has(anfrageDialog.typId) && (
                <AnschreibenPicker
                  typId={anfrageDialog.typId}
                  auftragId={p.auftrag_id}
                  projektGemeinde={p.gemeinde || (p.adresse || '').split(',').pop()?.trim().replace(/^\d{5}\s*/, '')}
                  session={session}
                  workerUrl={workerUrl}
                />
              )}

              {/* Frist-Eingabe */}
              <div style={{ marginBottom: 'var(--space-4)' }}>
                <label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4, color: 'var(--text-secondary)' }}>
                  Frist bis (optional)
                </label>
                <input
                  type="date"
                  value={anfrageDialog.fristBis}
                  onChange={(e) => setAnfrageDialog(s => ({ ...s, fristBis: e.target.value }))}
                  style={{
                    padding: '8px 12px', width: '100%', boxSizing: 'border-box',
                    border: '1px solid var(--border-light)',
                    borderRadius: 'var(--radius-sm)', fontSize: 14,
                  }}
                />
              </div>

              {/* Aktionen */}
              <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
                <button className="btn btn-ghost" onClick={() => setAnfrageDialog(null)}>
                  Abbrechen
                </button>
                <button
                  className="btn btn-accent"
                  disabled={anfrageDialog.saving}
                  onClick={submitAnfrage}
                >
                  {anfrageDialog.saving ? 'Wird gespeichert...' : 'Als angefragt markieren'}
                </button>
              </div>
            </div>
          </div>
        </div>
      )}
    </>
  );

  // Layout: bei aktivem Inline-Viewer 2-Spalten wie in StammdatenTab.
  if (viewerActive) {
    return (
      <div style={{
        display: 'grid',
        gap: 'var(--space-5)',
        alignItems: 'start',
      }} className="responsive-split">
        <div>{dataColumn}</div>
        <div style={{ position: 'sticky', top: 'var(--space-4)' }}>
          <DokumentViewer
            dokumente={alleDokumenteFuerViewer}
            session={session}
            workerUrl={workerUrl}
            activeId={viewerDokId}
            onChangeActive={setViewerDokId}
          />
        </div>
      </div>
    );
  }
  return dataColumn;
};

// Hilfsfunktion: Kategorie für einen Dokumenttyp bestimmen
function getCategoryForType(typRaw, types) {
  if (!typRaw) return null;
  for (const group of types) {
    for (const item of group.items) {
      if (item.id === typRaw) return item.category || group.group || null;
    }
  }
  return null;
}

// ══════════════════════════════════════════════════════════════════
// ORTSTERMIN-TAB: vollständige Implementierung mit Voice/Rundgang
// ══════════════════════════════════════════════════════════════════

// ────────────────────────────────────────────────────────────────────
// Dokument-Viewer (iframe mit Signed URL)
// Controlled: activeId + onChangeActive Props steuern, welches Dokument gezeigt wird.
// Fallback: wenn activeId undefined, nutzt internen State + Auto-Select.
// ────────────────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════
// DokumentFelderPanel — zeigt die extrahierten Datenpunkte eines
// Dokuments und macht sie nachträglich editierbar. So "verschwinden"
// extrahierte Daten nicht in der DB, sondern bleiben pro Dokument
// einsehbar und korrigierbar.
// ══════════════════════════════════════════════════════════════════
const DokumentFelderPanel = ({ dokument, session, workerUrl, onSaved }) => {
  const showToast = useToast();
  const [editMode, setEditMode] = useState(false);
  const [draft, setDraft] = useState({});
  const [saving, setSaving] = useState(false);
  // Lokale Kopie der Felder: nach dem Speichern sofort aktualisiert,
  // damit die Anzeige nicht bis zum nächsten Reload veraltet bleibt.
  const [localFields, setLocalFields] = useState(null);

  const envelope = dokument?.extracted_fields;
  const fields = localFields || envelope?.fields || null;

  // Vollständige Feldliste: erwartete Felder aus dem Prompt-Schema (in
  // definierter Reihenfolge) + evtl. zusätzlich vorhandene Felder. So werden
  // auch NICHT extrahierte (leere) Felder angezeigt — der Nutzer kann sie
  // manuell aus dem PDF nachtragen. Die Felder sind pro Typ immer gleich.
  const skalarKeys = useMemo(() => {
    const skip = new Set(['beteiligte', 'eigentuemer', 'versteigerungsobjekte', 'einheiten', 'titel_vorschlag', 'notizen']);
    const erwartet = expectedFieldsFor(dokument?.typ_raw).filter(k => !skip.has(k));
    const vorhanden = fields
      ? Object.keys(fields).filter(k => !skip.has(k) && fields[k] != null && typeof fields[k] !== 'object')
      : [];
    // Erwartete zuerst (Schema-Reihenfolge), dann Extras die nur in fields stehen
    const result = [...erwartet];
    for (const k of vorhanden) if (!result.includes(k)) result.push(k);
    return result;
  }, [fields, dokument?.typ_raw]);

  useEffect(() => {
    // Draft + lokale Kopie zurücksetzen wenn Dokument wechselt
    setEditMode(false);
    setDraft({});
    setLocalFields(null);
  }, [dokument?.id]);

  if (skalarKeys.length === 0) {
    return (
      <div style={{ padding: 'var(--space-3) var(--space-4)', fontSize: 12, color: 'var(--text-tertiary)' }}>
        Für dieses Dokument liegen keine extrahierten Datenpunkte vor.
      </div>
    );
  }

  const startEdit = () => {
    const d = {};
    for (const k of skalarKeys) d[k] = (fields && fields[k] != null) ? String(fields[k]) : '';
    setDraft(d);
    setEditMode(true);
  };

  const save = async () => {
    setSaving(true);
    try {
      // Nur geänderte Felder senden
      const changed = {};
      for (const k of skalarKeys) {
        const neu = draft[k] === '' ? null : draft[k];
        const alt = (fields && fields[k] != null) ? String(fields[k]) : null;
        if (String(neu) !== String(alt)) changed[k] = neu;
      }
      if (Object.keys(changed).length === 0) {
        setEditMode(false);
        setSaving(false);
        return;
      }
      const res = await fetch(`${workerUrl}/api/dokument/felder-update`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
        body: JSON.stringify({ dokument_id: dokument.id, fields: changed }),
      });
      if (!res.ok) {
        let msg = 'Speichern fehlgeschlagen';
        try { const e = await res.json(); if (e.error) msg = e.error; } catch {}
        throw new Error(msg);
      }
      const result = await res.json();
      // Lokale Kopie sofort aktualisieren (Endpoint liefert gemergte fields zurück)
      if (result?.fields) setLocalFields(result.fields);
      showToast('Datenpunkte gespeichert', 'success');
      setEditMode(false);
      if (onSaved) onSaved();
    } catch (err) {
      showToast(err.message || 'Speichern fehlgeschlagen', 'error');
    } finally {
      setSaving(false);
    }
  };

  return (
    <div style={{ borderTop: '1px solid var(--border-light)', background: 'var(--surface)' }}>
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        padding: 'var(--space-2) var(--space-3)',
      }}>
        <span style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-tertiary)' }}>
          Datenpunkte ({skalarKeys.filter(k => fields && fields[k] != null && fields[k] !== '').length}/{skalarKeys.length} erfasst)
        </span>
        {!editMode ? (
          <button onClick={startEdit}
            style={{ background: 'none', border: 'none', color: 'var(--vl-blue)', fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
            Bearbeiten
          </button>
        ) : (
          <div style={{ display: 'flex', gap: 'var(--space-3)' }}>
            <button onClick={() => setEditMode(false)} disabled={saving}
              style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', fontSize: 12, cursor: 'pointer' }}>
              Abbrechen
            </button>
            <button onClick={save} disabled={saving}
              style={{ background: 'none', border: 'none', color: 'var(--vl-blue)', fontSize: 12, fontWeight: 700, cursor: 'pointer' }}>
              {saving ? 'Speichert…' : 'Speichern'}
            </button>
          </div>
        )}
      </div>
      <div style={{ maxHeight: 280, overflowY: 'auto', padding: '0 var(--space-3) var(--space-3)' }}>
        {skalarKeys.map(k => {
          const label = FELD_LABELS[k] || k;
          const val = fields ? fields[k] : null;
          const istLeer = val == null || val === '';
          const isLong = typeof val === 'string' && val.length > 60;
          return (
            <div key={k} style={{
              display: 'flex', flexDirection: (editMode && isLong) ? 'column' : 'row',
              gap: editMode && isLong ? 4 : 8,
              padding: '6px 0', borderBottom: '1px solid var(--border-light)',
              alignItems: editMode && isLong ? 'stretch' : 'flex-start',
            }}>
              <span style={{ fontSize: 12, color: 'var(--text-tertiary)', flex: editMode && isLong ? undefined : '0 0 42%', fontWeight: 500 }}>
                {label}
              </span>
              {!editMode ? (
                <span style={{ fontSize: 12, color: istLeer ? 'var(--text-tertiary)' : 'var(--text-primary)', fontStyle: istLeer ? 'italic' : 'normal', flex: 1, wordBreak: 'break-word' }}>
                  {istLeer ? 'nicht erfasst' : String(val)}
                </span>
              ) : isLong ? (
                <textarea
                  value={draft[k] ?? ''}
                  onChange={e => setDraft(d => ({ ...d, [k]: e.target.value }))}
                  rows={3}
                  style={{ fontSize: 12, padding: '4px 6px', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)', width: '100%', resize: 'vertical', fontFamily: 'inherit' }}
                />
              ) : (
                <input
                  type="text"
                  value={draft[k] ?? ''}
                  onChange={e => setDraft(d => ({ ...d, [k]: e.target.value }))}
                  style={{ fontSize: 12, padding: '4px 6px', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)', flex: 1, minWidth: 0 }}
                />
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
};

const DokumentViewer = ({ dokumente, session, workerUrl, activeId, onChangeActive, placeholder, pdfOnly = false, expanded = false, onToggleExpanded = null }) => {
  const [internalId, setInternalId] = useState(null);
  const [url, setUrl] = useState(null);
  const [loading, setLoading] = useState(false);

  // Controlled vs uncontrolled: activeId hat Vorrang wenn explizit gesetzt
  const isControlled = activeId !== undefined;
  const selectedId = isControlled ? activeId : internalId;
  const setSelectedId = isControlled
    ? (id) => onChangeActive?.(id)
    : setInternalId;

  useEffect(() => {
    if (!selectedId) { setUrl(null); return; }
    let active = true;
    setLoading(true);
    ladeDokumentUrl(selectedId, session, workerUrl)
      .then(u => { if (active) { setUrl(u); setLoading(false); } })
      .catch(() => { if (active) setLoading(false); });
    return () => { active = false; };
  }, [selectedId, session, workerUrl]);

  // Auto-select nur im uncontrolled-Modus
  useEffect(() => {
    if (isControlled) return;
    if (dokumente.length > 0 && !internalId) {
      const preferred = dokumente.find(d => d.scope === 'objekt' || d.scope === 'gutachten')
                     || dokumente[0];
      if (preferred?.id) setInternalId(preferred.id);
    }
  }, [dokumente, internalId, isControlled]);

  if (dokumente.length === 0) {
    return (
      <div style={{
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
        padding: 'var(--space-8)', color: 'var(--text-tertiary)', fontSize: 14,
        background: 'var(--surface-light)', borderRadius: 'var(--radius-md)',
        height: 'calc(100vh - 160px)', minHeight: 400, border: '1px dashed var(--border-light)',
      }}>
        <IconDocument size={48} stroke="var(--text-tertiary)" />
        <div style={{ marginTop: 'var(--space-3)', fontSize: 14 }}>
          Noch keine passenden Dokumente
        </div>
        <div style={{ fontSize: 12, marginTop: 4, textAlign: 'center', maxWidth: 260 }}>
          {placeholder || 'Lade ein passendes Dokument hoch, um es hier zu referenzieren.'}
        </div>
      </div>
    );
  }

  if (!selectedId) {
    return (
      <div style={{
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
        padding: 'var(--space-8)', color: 'var(--text-tertiary)', fontSize: 14,
        background: 'var(--surface-light)', borderRadius: 'var(--radius-md)',
        height: 'calc(100vh - 160px)', minHeight: 400, border: '1px dashed var(--border-light)',
      }}>
        <IconEye size={40} stroke="var(--text-tertiary)" />
        <div style={{ marginTop: 'var(--space-3)', fontSize: 14, textAlign: 'center', maxWidth: 280 }}>
          {placeholder || 'Klicke links auf einen Dokument-Verweis, um ihn hier anzuzeigen.'}
        </div>
      </div>
    );
  }

  return (
    <div style={{
      display: 'flex', flexDirection: 'column',
      height: 'calc(100vh - 160px)', minHeight: 500,
      border: '1px solid var(--border-light)',
      borderRadius: 'var(--radius-md)',
      background: 'var(--surface-light)', overflow: 'hidden',
    }}>
      <div style={{
        padding: 'var(--space-2) var(--space-3)',
        borderBottom: '1px solid var(--glass-border)',
        background: 'var(--glass-bg)',
        WebkitBackdropFilter: 'saturate(160%) blur(14px)',
        backdropFilter: 'saturate(160%) blur(14px)',
        boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.5)',
        display: 'flex', gap: 'var(--space-2)', alignItems: 'center',
      }}>
        <label style={{ fontSize: 12, color: 'var(--text-secondary)', whiteSpace: 'nowrap' }}>
          Dokument:
        </label>
        <select
          className="form-select"
          value={selectedId || ''}
          onChange={e => setSelectedId(e.target.value || null)}
          style={{ flex: 1, fontSize: 13, padding: '4px 8px' }}
        >
          {dokumente.map(d => (
            <option key={d.id} value={d.id}>
              {d.label} — {d.typ}
            </option>
          ))}
        </select>
        {onToggleExpanded && (
          <button
            type="button"
            onClick={onToggleExpanded}
            title={expanded ? 'Geteilte Ansicht' : 'PDF vergrößern'}
            style={{
              background: 'none', border: '1px solid var(--border-light)',
              borderRadius: 'var(--radius-sm)', padding: '4px 6px', cursor: 'pointer',
              color: 'var(--text-secondary)', flexShrink: 0,
            }}
          >
            {expanded ? (
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="4 14 10 14 10 20" /><polyline points="20 10 14 10 14 4" /><line x1="14" y1="10" x2="21" y2="3" /><line x1="3" y1="21" x2="10" y2="14" /></svg>
            ) : (
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 3 21 3 21 9" /><polyline points="9 21 3 21 3 15" /><line x1="21" y1="3" x2="14" y2="10" /><line x1="3" y1="21" x2="10" y2="14" /></svg>
            )}
          </button>
        )}
        {isControlled && (
          <button
            type="button"
            onClick={() => setSelectedId(null)}
            title="Viewer schließen"
            style={{
              background: 'none', border: '1px solid var(--border-light)',
              borderRadius: 'var(--radius-sm)', padding: '4px 6px', cursor: 'pointer',
              color: 'var(--text-secondary)',
            }}
          >
            <IconClose size={14} />
          </button>
        )}
      </div>
      <div style={{ flex: 1, background: '#fafaf8', position: 'relative', minHeight: 0 }}>
        {loading && (
          <div style={{
            position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
            color: 'var(--text-tertiary)', fontSize: 13,
          }}>
            Lade Dokument…
          </div>
        )}
        {url && (
          <>
            <iframe
              key={url}
              src={url}
              style={{
                display: 'block',
                width: '100%',
                height: '100%',
                border: 'none',
                background: '#fafaf8',
              }}
              title="Dokument"
            />
            <a
              href={url}
              target="_blank"
              rel="noopener noreferrer"
              title="In neuem Tab öffnen"
              style={{
                position: 'absolute', top: 8, right: 8,
                background: 'rgba(255,255,255,0.95)',
                border: '1px solid var(--border-light)',
                borderRadius: 'var(--radius-sm)',
                padding: '6px 10px', fontSize: 11, fontWeight: 600,
                color: 'var(--vl-blue, #0A2540)',
                textDecoration: 'none',
                display: 'inline-flex', alignItems: 'center', gap: 4,
                boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
              }}
            >
              <IconExternalLink size={12} />
              Neuer Tab
            </a>
          </>
        )}
      </div>
      {/* Extrahierte Datenpunkte des gewählten Dokuments — nur wenn nicht
          als reine PDF-Referenz genutzt (sonst doppelt zur linken Spalte) */}
      {!pdfOnly && (() => {
        const aktuellesDok = dokumente.find(d => d.id === selectedId);
        if (!aktuellesDok) return null;
        return (
          <DokumentFelderPanel
            dokument={aktuellesDok}
            session={session}
            workerUrl={workerUrl}
            onSaved={onChangeActive ? () => onChangeActive(selectedId) : undefined}
          />
        );
      })()}
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// Einzelne Notiz-Card (mit Edit, Delete, Foto-Anzeige)
// ────────────────────────────────────────────────────────────────────
const NoteCard = ({ note, session, workerUrl, onEdit, onDelete, objekte }) => {
  const [editing, setEditing] = useState(false);
  const [editText, setEditText] = useState(note.text || '');
  const [photoUrl, setPhotoUrl] = useState(null);
  // Rohtranskripte sind lang und nur Beleg → standardmäßig eingeklappt
  const [rawExpanded, setRawExpanded] = useState(false);

  // Foto-URL lazily laden (signed URL)
  useEffect(() => {
    if (note.type !== 'photo' || !note.image_url) return;
    let active = true;
    (async () => {
      const sb = await initSupabase();
      const { data } = await sb.storage.from('ortstermin-photos').createSignedUrl(note.image_url, 600);
      if (active && data?.signedUrl) setPhotoUrl(data.signedUrl);
    })();
    return () => { active = false; };
  }, [note.image_url, note.type]);

  const kapitel = VL_KAPITEL_TAGS.find(k => k.id === note.kapitel);
  const objekt = note.objekt_id ? objekte.find(o => o.id === note.objekt_id) : null;

  const handleSaveEdit = async () => {
    if (editText.trim() === (note.text || '').trim()) {
      setEditing(false);
      return;
    }
    try {
      await onEdit(note.id, { text: editText.trim() });
      setEditing(false);
    } catch (e) {
      alert('Fehler beim Speichern: ' + (e.message || e));
    }
  };

  // ─── Sonderfall: Rohtranskript (Original, wortgetreu) ───
  // Wird klar abgesetzt und einklappbar dargestellt — es ist der Beleg,
  // nicht der Arbeitstext. Die thematisch sortierten Voice-Segmente stehen
  // separat darunter.
  if (note.kapitel === 'transcript_raw') {
    const charCount = (note.text || '').length;
    return (
      <div style={{
        padding: 'var(--space-3) var(--space-4)',
        background: 'var(--surface-light)',
        border: '1px dashed var(--border-medium)',
        borderRadius: 'var(--radius-md)',
        marginBottom: 'var(--space-2)',
      }}>
        <div
          onClick={() => setRawExpanded(v => !v)}
          style={{
            display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer',
            fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)',
          }}
        >
          <span style={{
            display: 'inline-block', transform: rawExpanded ? 'rotate(90deg)' : 'none',
            transition: 'transform 0.15s ease', fontSize: 11,
          }}>▶</span>
          <span>Original-Transkript (wortgetreu)</span>
          <span style={{ fontWeight: 400, color: 'var(--text-tertiary)', fontSize: 11 }}>
            {charCount.toLocaleString('de-DE')} Zeichen · Beleg, nicht für Gutachten-Text
          </span>
          <span style={{ marginLeft: 'auto', color: 'var(--text-tertiary)', fontSize: 11 }}>
            {note.created_at ? new Date(note.created_at).toLocaleString('de-DE', {
              day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
            }) : ''}
          </span>
        </div>
        {rawExpanded && (
          <>
            {editing ? (
              <>
                <textarea
                  value={editText}
                  onChange={e => setEditText(e.target.value)}
                  style={{
                    width: '100%', minHeight: 200, padding: 'var(--space-2)',
                    marginTop: 'var(--space-2)',
                    border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
                    fontFamily: 'inherit', fontSize: 13, resize: 'vertical', lineHeight: 1.6,
                  }}
                  autoFocus
                />
                <div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-2)' }}>
                  <button className="btn btn-primary btn-sm" onClick={handleSaveEdit}>Speichern</button>
                  <button className="btn btn-ghost btn-sm" onClick={() => { setEditing(false); setEditText(note.text || ''); }}>Abbrechen</button>
                </div>
              </>
            ) : (
              <>
                <div style={{
                  fontSize: 13, lineHeight: 1.6, whiteSpace: 'pre-wrap',
                  color: 'var(--text-secondary)', marginTop: 'var(--space-3)',
                  paddingTop: 'var(--space-3)', borderTop: '1px solid var(--border-light)',
                }}>
                  {note.text || '(leer)'}
                </div>
                <div style={{ display: 'flex', gap: 'var(--space-3)', marginTop: 'var(--space-2)', fontSize: 11 }}>
                  <button onClick={() => setEditing(true)}
                    style={{ background: 'none', border: 'none', color: 'var(--vl-blue-light)', cursor: 'pointer', fontSize: 11 }}>
                    Bearbeiten
                  </button>
                  <button onClick={() => onDelete(note.id)}
                    style={{ background: 'none', border: 'none', color: 'var(--danger)', cursor: 'pointer', fontSize: 11 }}>
                    Löschen
                  </button>
                </div>
              </>
            )}
          </>
        )}
      </div>
    );
  }

  return (
    <div style={{
      padding: 'var(--space-3) var(--space-4)',
      background: 'var(--surface)',
      border: '1px solid var(--border-light)',
      borderRadius: 'var(--radius-md)',
      marginBottom: 'var(--space-2)',
    }}>
      <div style={{
        display: 'flex', gap: 'var(--space-2)', alignItems: 'center',
        marginBottom: 'var(--space-2)', flexWrap: 'wrap', fontSize: 11,
      }}>
        {kapitel && (
          <Pill variant="blue" style={{ fontSize: 10 }}>
            {kapitel.label}
          </Pill>
        )}
        {objekt && (
          <Pill variant="orange" style={{ fontSize: 10 }}>
            {objekt.bezeichnung}
          </Pill>
        )}
        {note.type === 'photo' && (
          <Pill style={{ fontSize: 10 }}>Foto</Pill>
        )}
        {note.type === 'voice' && (
          <Pill style={{ fontSize: 10 }}>Voice</Pill>
        )}
        <span style={{ marginLeft: 'auto', color: 'var(--text-tertiary)' }}>
          {new Date(note.created_at).toLocaleString('de-DE', {
            day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
          })}
        </span>
      </div>

      {photoUrl && (
        <img src={photoUrl} alt="Notiz-Foto" style={{
          maxWidth: '100%', maxHeight: 240, borderRadius: 'var(--radius-sm)',
          marginBottom: note.text ? 'var(--space-2)' : 0,
        }} />
      )}

      {editing ? (
        <>
          <textarea
            value={editText}
            onChange={e => setEditText(e.target.value)}
            style={{
              width: '100%', minHeight: 80, padding: 'var(--space-2)',
              border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
              fontFamily: 'inherit', fontSize: 14, resize: 'vertical',
            }}
            autoFocus
          />
          <div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-2)' }}>
            <button className="btn btn-primary btn-sm" onClick={handleSaveEdit}>Speichern</button>
            <button className="btn btn-ghost btn-sm" onClick={() => { setEditing(false); setEditText(note.text || ''); }}>
              Abbrechen
            </button>
          </div>
        </>
      ) : (
        <>
          <div style={{
            fontSize: 14, lineHeight: 1.5, whiteSpace: 'pre-wrap',
            color: note.text ? 'var(--text-primary)' : 'var(--text-tertiary)',
          }}>
            {note.text || '(Leere Notiz)'}
          </div>
          <div style={{
            display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-2)',
            fontSize: 11, color: 'var(--text-tertiary)',
          }}>
            <button
              onClick={() => setEditing(true)}
              style={{ background: 'none', border: 'none', color: 'var(--vl-blue-light)', cursor: 'pointer', fontSize: 11 }}
            >
              Bearbeiten
            </button>
            <button
              onClick={() => {
                if (confirm('Notiz löschen?')) onDelete(note.id);
              }}
              style={{ background: 'none', border: 'none', color: 'var(--danger)', cursor: 'pointer', fontSize: 11 }}
            >
              Löschen
            </button>
          </div>
        </>
      )}
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// ────────────────────────────────────────────────────────────────────
// FotoZuordnung — Fotos den DOCX-Template-Platzhaltern zuordnen
// ────────────────────────────────────────────────────────────────────

const FOTO_SLOTS = [
  { id: 'foto_titelbild', label: 'Titelbild' },
  { id: 'foto_lagekarte', label: 'Lagekarte' },
  { id: 'foto_mikrolage', label: 'Mikrolage-Karte' },
  { id: 'foto_lageplan', label: 'Lageplan' },
  { id: 'foto_luftbild', label: 'Luftbild' },
  { id: 'foto_bebauungsplan', label: 'Bebauungsplan' },
  { id: 'foto_laerm_lden', label: 'Lärmkarte LDEN' },
  { id: 'foto_laerm_lnight', label: 'Lärmkarte LNight' },
];

// ────────────────────────────────────────────────────────────────────
// FotoEditor — Bild-Annotation: Pfeile, Markierungen, Textkästen → zurückspeichern.
// Arbeitet in NATÜRLICHEN Bildkoordinaten (SVG viewBox = Originalmaße) → pixelgenauer
// Export. Bild wird als Blob→ObjectURL geladen (sonst taintet der Canvas-Export).
// ────────────────────────────────────────────────────────────────────
const FOTO_EDITOR_FARBEN = [
  { name: 'Rot', value: '#E5342B' },
  { name: 'Gelb', value: '#F2C200' },
  { name: 'Blau', value: '#2D6CDF' },
  { name: 'Weiß', value: '#FFFFFF' },
  { name: 'Schwarz', value: '#1D1D1F' },
];

// Strichstärke-Stufen (Multiplikator auf die bildabhängige Basisstärke) — für Pfeile + Markierungs-Rahmen
const FOTO_EDITOR_STAERKEN = [
  { name: 'Dünn', mult: 0.6 },
  { name: 'Mittel', mult: 1 },
  { name: 'Dick', mult: 1.8 },
  { name: 'Sehr dick', mult: 2.8 },
];

const FotoEditor = ({ imageUrl, projektId, gutachtenId, session, workerUrl, title, onClose, onSaved, watermarkText = 'VÖLKEL · LANG', z = 2000 }) => {
  const [blobUrl, setBlobUrl] = useState(null);
  const [nat, setNat] = useState(null);            // { w, h } Originalmaße
  const [tool, setTool] = useState('arrow');       // 'arrow' | 'highlight' | 'text' | 'select'
  const [color, setColor] = useState('#E5342B');
  const [weight, setWeight] = useState(1);         // aktive Strichstärke (Multiplikator) für neue/ausgewählte Linien-Elemente
  const [items, setItems] = useState([]);
  const [selectedId, setSelectedId] = useState(null);
  const [draft, setDraft] = useState(null);
  const [saving, setSaving] = useState(false);
  const [loadError, setLoadError] = useState(false);
  const [frame, setFrame] = useState(false); // dünner grauer Rahmen ums Bild
  const imgElRef = useRef(null);
  const svgRef = useRef(null);
  const measureRef = useRef(null);
  const dragRef = useRef(null);

  useEffect(() => {
    let active = true; let urlToRevoke = null;
    (async () => {
      try {
        const res = await fetch(imageUrl);
        const blob = await res.blob();
        const url = URL.createObjectURL(blob); urlToRevoke = url;
        const img = new Image();
        img.onload = () => { if (!active) return; imgElRef.current = img; setNat({ w: img.naturalWidth, h: img.naturalHeight }); setBlobUrl(url); };
        img.onerror = () => { if (active) setLoadError(true); };
        img.src = url;
      } catch { if (active) setLoadError(true); }
    })();
    return () => { active = false; if (urlToRevoke) URL.revokeObjectURL(urlToRevoke); };
  }, [imageUrl]);

  const strokeW = nat ? Math.max(3, Math.round(nat.w / 280)) : 4;
  const fontPx = nat ? Math.max(18, Math.round(nat.w / 38)) : 24;
  const handleR = nat ? Math.max(strokeW * 2.6, Math.round(nat.w / 90)) : 8;
  const arrowHitTol = nat ? Math.max(strokeW * 6, Math.round(nat.w / 120)) : 9;
  const wmFont = nat ? Math.max(28, Math.round(nat.w / 16)) : 40;
  const capFont = nat ? Math.max(15, Math.round(nat.w / 50)) : 18;
  const frameW = nat ? Math.max(3, Math.round(nat.w / 300)) : 3;

  const measureText = (text, px = fontPx) => {
    if (!measureRef.current) measureRef.current = document.createElement('canvas');
    const ctx = measureRef.current.getContext('2d');
    ctx.font = `600 ${px}px Arial, sans-serif`;
    return ctx.measureText(text || '').width;
  };
  const textColorFor = (bg) => {
    const c = bg.replace('#', ''); const r = parseInt(c.substr(0,2),16), g = parseInt(c.substr(2,2),16), b = parseInt(c.substr(4,2),16);
    return (0.299*r + 0.587*g + 0.114*b) > 160 ? '#1D1D1F' : '#FFFFFF';
  };
  const toNat = (e) => {
    const svg = svgRef.current; if (!svg || !nat) return { x: 0, y: 0 };
    const r = svg.getBoundingClientRect();
    const cx = e.clientX, cy = e.clientY;
    return {
      x: Math.max(0, Math.min(nat.w, (cx - r.left) * (nat.w / r.width))),
      y: Math.max(0, Math.min(nat.h, (cy - r.top) * (nat.h / r.height))),
    };
  };
  // Pfeil-Geometrie: Schaft endet an der Kopfbasis (kein runder Überstand an der Spitze),
  // Kopf als sauberes, proportionales Dreieck.
  const arrowGeom = (x1, y1, x2, y2, sw = strokeW) => {
    const dx = x2 - x1, dy = y2 - y1;
    const L = Math.hypot(dx, dy) || 1;
    const ux = dx / L, uy = dy / L;          // Richtung
    const px = -uy, py = ux;                 // Senkrechte
    const headLen = Math.min(sw * 4, L * 0.9);
    const headHW = headLen * 0.55;           // halbe Kopfbreite
    const bx = x2 - ux * headLen, by = y2 - uy * headLen;  // Basismitte
    return {
      sx: bx, sy: by,                        // Schaft endet hier
      head: [[x2, y2], [bx + px * headHW, by + py * headHW], [bx - px * headHW, by - py * headHW]],
    };
  };
  const textBox = (it) => ({ x: it.x, y: it.y, w: measureText(it.text) + fontPx*0.7, h: fontPx*1.5 });
  const captionBox = (it) => ({ x: it.x, y: it.y, w: measureText(it.text, capFont) + capFont*0.9, h: capFont*1.7 });
  // Effektive Strichstärke eines Elements (Basisstärke × individueller Multiplikator)
  const ew = (it) => strokeW * ((it && it.weight) || 1);
  const itemHandleR = (it) => (it && it.type === 'arrow') ? Math.max(handleR, ew(it) * 1.3) : handleR;

  const addItem = (it) => { const id = `a_${Date.now()}_${Math.random().toString(36).slice(2,5)}`; setItems(prev => [...prev, { id, ...it }]); setSelectedId(id); };
  const addWatermark = () => { if (nat) addItem({ type: 'watermark', x: Math.round(nat.w / 2), y: Math.round(nat.h / 2), text: watermarkText, color }); };
  const undo = () => { setItems(prev => prev.slice(0, -1)); setSelectedId(null); };
  const delSelected = () => { if (selectedId) { setItems(prev => prev.filter(i => i.id !== selectedId)); setSelectedId(null); } };

  const hitTest = (it, p) => {
    if (it.type === 'highlight') return p.x >= it.x && p.x <= it.x+it.w && p.y >= it.y && p.y <= it.y+it.h;
    if (it.type === 'text') { const b = textBox(it); return p.x >= b.x && p.x <= b.x+b.w && p.y >= b.y-b.h && p.y <= b.y; }
    if (it.type === 'caption') { const b = captionBox(it); return p.x >= b.x && p.x <= b.x+b.w && p.y >= b.y-b.h && p.y <= b.y; }
    if (it.type === 'arrow') {
      const Ax=it.x1,Ay=it.y1,Bx=it.x2,By=it.y2; const L2 = (Bx-Ax)**2 + (By-Ay)**2 || 1;
      const t = Math.max(0, Math.min(1, ((p.x-Ax)*(Bx-Ax) + (p.y-Ay)*(By-Ay)) / L2));
      return Math.hypot(p.x - (Ax+t*(Bx-Ax)), p.y - (Ay+t*(By-Ay))) < Math.max(arrowHitTol, ew(it) * 1.6);
    }
    if (it.type === 'watermark') {
      const w = measureText(it.text, wmFont), h = wmFont;
      return p.x >= it.x - w/2 && p.x <= it.x + w/2 && p.y >= it.y - h/2 && p.y <= it.y + h/2;
    }
    return false;
  };
  const moveItem = (it, dx, dy) => it.type === 'arrow'
    ? { ...it, x1: it.x1+dx, y1: it.y1+dy, x2: it.x2+dx, y2: it.y2+dy }
    : { ...it, x: it.x+dx, y: it.y+dy };

  // Griff-Treffer am ausgewählten Element: Endpunkte (Pfeil) bzw. Ecken (Markierung).
  const handleHit = (it, p) => {
    if (!it) return null;
    const near = (hx, hy) => Math.hypot(p.x - hx, p.y - hy) <= itemHandleR(it) * 1.4;
    if (it.type === 'arrow') {
      if (near(it.x1, it.y1)) return { mode: 'arrow-pt', id: it.id, pt: 'a' };
      if (near(it.x2, it.y2)) return { mode: 'arrow-pt', id: it.id, pt: 'b' };
    } else if (it.type === 'highlight') {
      const corners = [[it.x, it.y, it.x+it.w, it.y+it.h], [it.x+it.w, it.y, it.x, it.y+it.h], [it.x, it.y+it.h, it.x+it.w, it.y], [it.x+it.w, it.y+it.h, it.x, it.y]];
      for (const [cx, cy, fx, fy] of corners) if (near(cx, cy)) return { mode: 'hl-corner', id: it.id, fixed: { x: fx, y: fy } };
    }
    return null;
  };

  const onDown = (e) => {
    if (!nat) return;
    const p = toNat(e);
    // 1) Griff des ausgewählten Elements? → Umformen
    const selItem = items.find(i => i.id === selectedId);
    const grip = handleHit(selItem, p);
    if (grip) { dragRef.current = grip; return; }
    // 2) Vorhandenes Element getroffen → auswählen + verschieben (in JEDEM Werkzeug)
    const hit = [...items].reverse().find(it => hitTest(it, p));
    if (hit) { setSelectedId(hit.id); dragRef.current = { mode: 'move', id: hit.id, start: p, orig: hit }; return; }
    // 3) Leere Fläche → je nach Werkzeug
    if (tool === 'select') { setSelectedId(null); return; }
    if (tool === 'text') {
      const text = window.prompt('Text für die Markierung:'); if (!text) return;
      addItem({ type: 'text', x: p.x, y: p.y + fontPx, text, color });
      return;
    }
    if (tool === 'caption') {
      const text = window.prompt('Text für den Infokasten:'); if (!text) return;
      addItem({ type: 'caption', x: p.x, y: p.y + capFont, text });
      return;
    }
    setSelectedId(null);
    setDraft({ type: tool, x1: p.x, y1: p.y, x2: p.x, y2: p.y, color, weight });
  };
  const onMove = (e) => {
    const d = dragRef.current;
    if (d) {
      const p = toNat(e);
      if (d.mode === 'move') {
        const dx = p.x - d.start.x, dy = p.y - d.start.y;
        setItems(prev => prev.map(it => it.id === d.id ? moveItem(d.orig, dx, dy) : it));
      } else if (d.mode === 'arrow-pt') {
        setItems(prev => prev.map(it => it.id === d.id ? (d.pt === 'a' ? { ...it, x1: p.x, y1: p.y } : { ...it, x2: p.x, y2: p.y }) : it));
      } else if (d.mode === 'hl-corner') {
        const x = Math.min(p.x, d.fixed.x), y = Math.min(p.y, d.fixed.y), w = Math.abs(p.x - d.fixed.x), hh = Math.abs(p.y - d.fixed.y);
        setItems(prev => prev.map(it => it.id === d.id ? { ...it, x, y, w, h: hh } : it));
      }
      return;
    }
    if (!draft) return;
    const p = toNat(e); setDraft(prev => ({ ...prev, x2: p.x, y2: p.y }));
  };
  const onUp = () => {
    if (dragRef.current) { dragRef.current = null; return; }
    if (!draft) return;
    const { type, x1, y1, x2, y2, color: c, weight: wgt } = draft;
    if (type === 'arrow' && Math.hypot(x2-x1, y2-y1) > strokeW*2) addItem({ type: 'arrow', x1, y1, x2, y2, color: c, weight: wgt });
    else if (type === 'highlight') {
      const x = Math.min(x1,x2), y = Math.min(y1,y2), w = Math.abs(x2-x1), h = Math.abs(y2-y1);
      if (w > strokeW*2 && h > strokeW*2) addItem({ type: 'highlight', x, y, w, h, color: c, weight: wgt });
    }
    setDraft(null);
  };

  useEffect(() => {
    const onKey = (e) => {
      if ((e.key === 'Delete' || e.key === 'Backspace') && selectedId) { e.preventDefault(); delSelected(); }
      if (e.key === 'Escape' && !saving) onClose();
    };
    window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
  }, [selectedId, saving, onClose]);

  const save = async () => {
    if (!imgElRef.current || !nat || saving) return;
    setSaving(true);
    try {
      const canvas = document.createElement('canvas'); canvas.width = nat.w; canvas.height = nat.h;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(imgElRef.current, 0, 0, nat.w, nat.h);
      ctx.lineJoin = 'round'; ctx.lineCap = 'round';
      for (const it of items) {
        if (it.type === 'arrow') {
          const sw = ew(it); const g = arrowGeom(it.x1, it.y1, it.x2, it.y2, sw);
          ctx.strokeStyle = it.color; ctx.fillStyle = it.color; ctx.lineWidth = sw; ctx.lineCap = 'round';
          ctx.beginPath(); ctx.moveTo(it.x1, it.y1); ctx.lineTo(g.sx, g.sy); ctx.stroke();
          ctx.beginPath(); ctx.moveTo(g.head[0][0], g.head[0][1]); ctx.lineTo(g.head[1][0], g.head[1][1]); ctx.lineTo(g.head[2][0], g.head[2][1]); ctx.closePath(); ctx.fill();
        } else if (it.type === 'highlight') {
          ctx.globalAlpha = 0.28; ctx.fillStyle = it.color; ctx.fillRect(it.x, it.y, it.w, it.h); ctx.globalAlpha = 1;
          ctx.strokeStyle = it.color; ctx.lineWidth = ew(it); ctx.strokeRect(it.x, it.y, it.w, it.h);
        } else if (it.type === 'text') {
          const b = textBox(it);
          ctx.fillStyle = it.color; ctx.fillRect(b.x, b.y - b.h, b.w, b.h);
          ctx.fillStyle = textColorFor(it.color); ctx.font = `600 ${fontPx}px Arial, sans-serif`; ctx.textBaseline = 'middle';
          ctx.fillText(it.text, b.x + fontPx*0.35, b.y - b.h/2);
        } else if (it.type === 'watermark') {
          ctx.save();
          ctx.font = `700 ${wmFont}px Arial, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
          ctx.lineWidth = Math.max(1, strokeW*0.4); ctx.strokeStyle = textColorFor(it.color); ctx.globalAlpha = 0.22; ctx.strokeText(it.text, it.x, it.y);
          ctx.fillStyle = it.color; ctx.globalAlpha = 0.42; ctx.fillText(it.text, it.x, it.y);
          ctx.restore();
        } else if (it.type === 'caption') {
          const b = captionBox(it); const r = Math.round(capFont*0.18);
          ctx.beginPath();
          if (ctx.roundRect) ctx.roundRect(b.x, b.y - b.h, b.w, b.h, r); else ctx.rect(b.x, b.y - b.h, b.w, b.h);
          ctx.fillStyle = '#F2F2F2'; ctx.fill();
          ctx.lineWidth = Math.max(1, Math.round(strokeW*0.5)); ctx.strokeStyle = '#8A8A8A'; ctx.stroke();
          ctx.fillStyle = '#1D1D1F'; ctx.font = `600 ${capFont}px Arial, sans-serif`; ctx.textBaseline = 'middle';
          ctx.fillText(it.text, b.x + capFont*0.45, b.y - b.h/2);
        }
      }
      if (frame) {
        ctx.strokeStyle = '#7A7A7A'; ctx.lineWidth = frameW;
        ctx.strokeRect(frameW/2, frameW/2, nat.w - frameW, nat.h - frameW);
      }
      const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.92));
      if (!blob) throw new Error('Export fehlgeschlagen');
      const { storage_path } = await uploadPhotoBlob(blob, projektId, gutachtenId, session, workerUrl);
      onSaved(storage_path);
    } catch (e) {
      alert('Speichern fehlgeschlagen: ' + (e.message || e));
    } finally { setSaving(false); }
  };

  const renderArrow = (it, sel) => {
    const sw = ew(it); const g = arrowGeom(it.x1, it.y1, it.x2, it.y2, sw); const hr = itemHandleR(it);
    return (
      <g key={it.id}>
        <line x1={it.x1} y1={it.y1} x2={g.sx} y2={g.sy} stroke={it.color} strokeWidth={sw} strokeLinecap="round" />
        <polygon points={g.head.map(pt => pt.join(',')).join(' ')} fill={it.color} />
        {sel && (
          <g>
            <circle cx={it.x1} cy={it.y1} r={hr} fill="#fff" stroke="var(--vl-blue)" strokeWidth={strokeW*0.8} />
            <circle cx={it.x2} cy={it.y2} r={hr} fill="#fff" stroke="var(--vl-blue)" strokeWidth={strokeW*0.8} />
          </g>
        )}
      </g>
    );
  };
  const renderHighlight = (it, sel) => (
    <g key={it.id}>
      <rect x={it.x} y={it.y} width={it.w} height={it.h} fill={it.color} fillOpacity={0.28} stroke={it.color} strokeWidth={ew(it)} />
      {sel && (
        <g>
          <rect x={it.x} y={it.y} width={it.w} height={it.h} fill="none" stroke="var(--vl-blue)" strokeWidth={strokeW*0.5} strokeDasharray={`${strokeW*2} ${strokeW*2}`} />
          {[[it.x, it.y], [it.x+it.w, it.y], [it.x, it.y+it.h], [it.x+it.w, it.y+it.h]].map(([cx, cy], i) => (
            <circle key={i} cx={cx} cy={cy} r={handleR} fill="#fff" stroke="var(--vl-blue)" strokeWidth={strokeW*0.8} />
          ))}
        </g>
      )}
    </g>
  );
  const renderText = (it, sel) => {
    const b = textBox(it);
    return (
      <g key={it.id}>
        <rect x={b.x} y={b.y - b.h} width={b.w} height={b.h} fill={it.color} rx={Math.round(fontPx*0.15)} />
        <text x={b.x + fontPx*0.35} y={b.y - b.h/2} fontFamily="Arial, sans-serif" fontWeight="600" fontSize={fontPx} fill={textColorFor(it.color)} dominantBaseline="middle">{it.text}</text>
        {sel && <rect x={b.x} y={b.y - b.h} width={b.w} height={b.h} fill="none" stroke="var(--vl-blue)" strokeWidth={strokeW*0.5} strokeDasharray={`${strokeW*2} ${strokeW*2}`} />}
      </g>
    );
  };
  const renderCaption = (it, sel) => {
    const b = captionBox(it);
    return (
      <g key={it.id}>
        <rect x={b.x} y={b.y - b.h} width={b.w} height={b.h} fill="#F2F2F2" stroke="#8A8A8A" strokeWidth={Math.max(1, Math.round(strokeW*0.5))} rx={Math.round(capFont*0.18)} />
        <text x={b.x + capFont*0.45} y={b.y - b.h/2} fontFamily="Arial, sans-serif" fontWeight="600" fontSize={capFont} fill="#1D1D1F" dominantBaseline="middle">{it.text}</text>
        {sel && <rect x={b.x} y={b.y - b.h} width={b.w} height={b.h} fill="none" stroke="var(--vl-blue)" strokeWidth={strokeW*0.5} strokeDasharray={`${strokeW*2} ${strokeW*2}`} />}
      </g>
    );
  };
  const renderWatermark = (it, sel) => {
    const w = measureText(it.text, wmFont), h = wmFont;
    return (
      <g key={it.id}>
        <text x={it.x} y={it.y} textAnchor="middle" dominantBaseline="middle"
          fontFamily="Arial, sans-serif" fontWeight="700" fontSize={wmFont}
          fill={it.color} fillOpacity={0.42}
          stroke={textColorFor(it.color)} strokeOpacity={0.22} strokeWidth={Math.max(1, strokeW*0.4)}>{it.text}</text>
        {sel && <rect x={it.x - w/2 - fontPx*0.2} y={it.y - h/2} width={w + fontPx*0.4} height={h} fill="none" stroke="var(--vl-blue)" strokeWidth={strokeW*0.5} strokeDasharray={`${strokeW*2} ${strokeW*2}`} />}
      </g>
    );
  };
  const renderItem = (it, sel) => it.type === 'arrow' ? renderArrow(it, sel) : it.type === 'highlight' ? renderHighlight(it, sel) : it.type === 'watermark' ? renderWatermark(it, sel) : it.type === 'caption' ? renderCaption(it, sel) : renderText(it, sel);

  // Aktives Farbfeld: Farbe des ausgewählten Elements, sonst die Farbe für neue Elemente.
  const selectedItem = items.find(i => i.id === selectedId) || null;
  const activeColor = selectedItem ? selectedItem.color : color;
  const applyColor = (value) => {
    setColor(value);
    if (selectedId) setItems(prev => prev.map(it => it.id === selectedId ? { ...it, color: value } : it));
  };
  // Strichstärke: für Pfeil/Markierung (ausgewähltes Element bzw. neue Elemente)
  const weightApplies = selectedItem ? (selectedItem.type === 'arrow' || selectedItem.type === 'highlight') : (tool === 'arrow' || tool === 'highlight');
  const activeWeight = (selectedItem && (selectedItem.type === 'arrow' || selectedItem.type === 'highlight')) ? (selectedItem.weight || 1) : weight;
  const applyWeight = (mult) => {
    setWeight(mult);
    if (selectedItem && (selectedItem.type === 'arrow' || selectedItem.type === 'highlight')) {
      setItems(prev => prev.map(it => it.id === selectedItem.id ? { ...it, weight: mult } : it));
    }
  };

  return (
    <div onClick={e => e.stopPropagation()} style={{ position: 'fixed', inset: 0, zIndex: z, background: 'rgba(0,0,0,0.85)', display: 'flex', flexDirection: 'column' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 14px', background: 'var(--surface)', borderBottom: '1px solid var(--border-light)', flexWrap: 'wrap' }}>
        <span style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)' }}>{title} bearbeiten</span>
        <div className="seg" role="tablist" aria-label="Werkzeug">
          {[['arrow', 'Pfeil'], ['highlight', 'Markierung'], ['text', 'Text'], ['caption', 'Infokasten'], ['select', 'Auswählen']].map(([t, l]) => (
            <button key={t} role="tab" aria-selected={tool === t} className={tool === t ? 'is-active' : ''} onClick={() => setTool(t)}>{l}</button>
          ))}
        </div>
        <button className="btn btn-ghost btn-sm" onClick={addWatermark} disabled={!nat} title="Wasserzeichen einfügen (verschiebbar)" style={{ fontSize: 12 }}>＋ Wasserzeichen</button>
        <button className="btn btn-ghost btn-sm" onClick={() => setFrame(f => !f)} aria-pressed={frame} disabled={!nat} title="Dünnen grauen Rahmen ums Bild ein-/ausblenden" style={{ fontSize: 12, color: frame ? 'var(--vl-blue)' : undefined, fontWeight: frame ? 600 : 400 }}>{frame ? '☑' : '☐'} Rahmen</button>
        <div style={{ display: 'flex', gap: 6 }}>
          {FOTO_EDITOR_FARBEN.map(f => (
            <button key={f.value} title={selectedId ? `Auswahl ${f.name} färben` : f.name} onClick={() => applyColor(f.value)}
              style={{ width: 24, height: 24, borderRadius: '50%', background: f.value, border: activeColor === f.value ? '3px solid var(--vl-blue)' : '1px solid var(--border-medium)', cursor: 'pointer', padding: 0 }} />
          ))}
        </div>
        <div style={{ display: 'flex', gap: 4, alignItems: 'center', opacity: weightApplies ? 1 : 0.4 }} title={weightApplies ? 'Strichstärke (Pfeil/Markierung)' : 'Strichstärke gilt für Pfeil und Markierung'}>
          {FOTO_EDITOR_STAERKEN.map(s => (
            <button key={s.mult} title={s.name} onClick={() => applyWeight(s.mult)} disabled={!weightApplies}
              style={{ width: 30, height: 24, borderRadius: 6, background: activeWeight === s.mult ? 'var(--surface-blue)' : 'var(--surface-light)', border: activeWeight === s.mult ? '2px solid var(--vl-blue)' : '1px solid var(--border-medium)', cursor: weightApplies ? 'pointer' : 'default', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <span style={{ display: 'block', width: 16, height: Math.max(2, Math.round(s.mult * 2.6)), background: 'var(--text-secondary)', borderRadius: 2 }} />
            </button>
          ))}
        </div>
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
          <button className="btn btn-ghost btn-sm" onClick={undo} disabled={items.length === 0}>Rückgängig</button>
          <button className="btn btn-ghost btn-sm" onClick={() => { setItems([]); setSelectedId(null); }} disabled={items.length === 0} title="Alle Markierungen entfernen (zurück zum Ausgangsbild)">Zurücksetzen</button>
          <button className="btn btn-ghost btn-sm" onClick={delSelected} disabled={!selectedId} style={{ color: selectedId ? 'var(--danger)' : undefined }}>Löschen</button>
          <button className="btn btn-ghost btn-sm" onClick={onClose} disabled={saving}>Abbrechen</button>
          <button className="btn btn-primary btn-sm" onClick={save} disabled={saving || !nat}>{saving ? 'Speichert…' : 'Speichern'}</button>
        </div>
      </div>
      <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16, overflow: 'auto' }}>
        {loadError ? (
          <div style={{ color: '#fff', fontSize: 14 }}>Bild konnte nicht geladen werden.</div>
        ) : (!blobUrl || !nat) ? (
          <div style={{ color: '#fff', fontSize: 14, display: 'flex', alignItems: 'center', gap: 8 }}><span className="entwurf-spinner" /> Lädt…</div>
        ) : (
          <div style={{ position: 'relative', display: 'inline-block', maxWidth: '100%', maxHeight: '100%' }}>
            <img src={blobUrl} alt="" style={{ display: 'block', maxWidth: 'min(1100px, 92vw)', maxHeight: '80vh', borderRadius: 6 }} />
            <svg ref={svgRef} viewBox={`0 0 ${nat.w} ${nat.h}`} preserveAspectRatio="none"
              style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', cursor: tool === 'select' ? 'default' : 'crosshair', touchAction: 'none' }}
              onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerLeave={onUp}>
              {items.map(it => renderItem(it, it.id === selectedId))}
              {draft && draft.type === 'arrow' && renderArrow({ ...draft, id: 'draft' }, false)}
              {draft && draft.type === 'highlight' && renderHighlight({ id: 'draft', x: Math.min(draft.x1, draft.x2), y: Math.min(draft.y1, draft.y2), w: Math.abs(draft.x2 - draft.x1), h: Math.abs(draft.y2 - draft.y1), color: draft.color }, false)}
              {frame && <rect x={frameW/2} y={frameW/2} width={nat.w - frameW} height={nat.h - frameW} fill="none" stroke="#7A7A7A" strokeWidth={frameW} />}
            </svg>
          </div>
        )}
      </div>
      <div style={{ padding: '8px 14px', background: 'var(--surface)', borderTop: '1px solid var(--border-light)', fontSize: 12, color: 'var(--text-tertiary)', textAlign: 'center' }}>
        {selectedId
          ? 'Ausgewählt: Griffe ziehen zum Umformen · ganzes Element ziehen zum Verschieben · „Löschen" (oder Entf-Taste) entfernt es.'
          : tool === 'arrow' ? 'Ziehen, um einen Pfeil zu zeichnen. Vorhandene Elemente lassen sich anklicken und bearbeiten.'
          : tool === 'highlight' ? 'Ziehen, um einen Bereich zu markieren. Vorhandene Elemente lassen sich anklicken und bearbeiten.'
          : tool === 'text' ? 'Klicken, um eine Textmarkierung zu setzen. Vorhandene Elemente lassen sich anklicken und bearbeiten.'
          : tool === 'caption' ? 'Klicken, um einen grauen Infokasten (z. B. Quelle) zu setzen. Vorhandene Elemente lassen sich anklicken und bearbeiten.'
          : 'Element antippen zum Auswählen, ziehen zum Verschieben, „Löschen" entfernt es.'}
      </div>
    </div>
  );
};

// Fullscreen-Viewer für ein zugeordnetes Foto (analog FotoLightbox)
const FotoZuordnung = ({ gutachten, session, workerUrl, projektId, onRefresh }) => {
  const [zuordnung, setZuordnung] = useState(gutachten.foto_zuordnung || {});
  const [ortsterminFotos, setOrtsterminFotos] = useState([]);
  const [loading, setLoading] = useState(true);
  const [expandedSlot, setExpandedSlot] = useState(null); // slot.id wenn Picker offen
  const [viewSlot, setViewSlot] = useState(null); // slot.id wenn Vorschau offen
  const [editorSlot, setEditorSlot] = useState(null); // slot.id wenn Bild-Editor offen
  const [signedUrls, setSignedUrls] = useState({});
  const [originalUrls, setOriginalUrls] = useState({}); // signierte URLs der gemerkten Originale (pro Slot)
  const [fotoUrls, setFotoUrls] = useState({}); // signed URLs für Ortstermin-Fotos (Picker)
  const fileInputRef = useRef(null);
  const [uploadSlot, setUploadSlot] = useState(null);

  useEffect(() => {
    setZuordnung(gutachten.foto_zuordnung || {});
  }, [gutachten.foto_zuordnung]);

  // Ortstermin-Fotos laden
  useEffect(() => {
    let active = true;
    ladeNotes(gutachten.id).then(notes => {
      if (active) {
        const photos = notes.filter(n => n.type === 'photo' && n.image_url);
        setOrtsterminFotos(photos);
        setLoading(false);
        // Thumbnails vorladen
        (async () => {
          const sb = await initSupabase();
          const urls = {};
          for (const p of photos) {
            try {
              const { data } = await sb.storage.from('ortstermin-photos').createSignedUrl(p.image_url, 600);
              if (data?.signedUrl && active) urls[p.id] = data.signedUrl;
            } catch {}
          }
          if (active) setFotoUrls(urls);
        })();
      }
    });
    return () => { active = false; };
  }, [gutachten.id]);

  // Signed URLs für zugeordnete Slots (+ gemerkte Originale)
  useEffect(() => {
    let active = true;
    (async () => {
      const sb = await initSupabase();
      const urls = {};
      for (const [slotId, path] of Object.entries(zuordnung)) {
        if (!path || slotId.startsWith('__') || typeof path !== 'string') continue; // Reserved-Keys (z. B. __originals) überspringen
        try {
          const { data } = await sb.storage.from('ortstermin-photos').createSignedUrl(path, 600);
          if (data?.signedUrl && active) urls[slotId] = data.signedUrl;
        } catch {}
      }
      if (active) setSignedUrls(urls);
      // Gemerkte Originale signieren (für „vom Original bearbeiten" + Wiederherstellen)
      const oUrls = {};
      for (const [slotId, path] of Object.entries(zuordnung.__originals || {})) {
        if (!path || typeof path !== 'string') continue;
        try {
          const { data } = await sb.storage.from('ortstermin-photos').createSignedUrl(path, 600);
          if (data?.signedUrl && active) oUrls[slotId] = data.signedUrl;
        } catch {}
      }
      if (active) setOriginalUrls(oUrls);
    })();
    return () => { active = false; };
  }, [zuordnung]);

  const saveZuordnung = async (next) => {
    setZuordnung(next);
    try {
      await apiPatchRow('gutachten', gutachten.id, { foto_zuordnung: next }, session, workerUrl);
      if (onRefresh) onRefresh();
    } catch (err) {
      console.error('Foto-Zuordnung speichern fehlgeschlagen:', err);
    }
  };

  const assignPhoto = async (slotId, storagePath) => {
    // Slot-Bild wechseln → evtl. gemerktes Original verwerfen (Bezug gilt nicht mehr)
    const origs = { ...(zuordnung.__originals || {}) };
    delete origs[slotId];
    const next = { ...zuordnung, [slotId]: storagePath };
    if (Object.keys(origs).length) next.__originals = origs; else delete next.__originals;
    await saveZuordnung(next);
    setExpandedSlot(null);
  };

  const removePhoto = async (slotId) => {
    const next = { ...zuordnung };
    delete next[slotId];
    const origs = { ...(next.__originals || {}) };
    delete origs[slotId];
    if (Object.keys(origs).length) next.__originals = origs; else delete next.__originals;
    await saveZuordnung(next);
  };

  // Slot auf das gemerkte, komplett unbearbeitete Original zurücksetzen
  const restoreOriginal = async (slotId) => {
    const origs = { ...(zuordnung.__originals || {}) };
    const origPath = origs[slotId];
    if (!origPath) return;
    delete origs[slotId];
    const next = { ...zuordnung, [slotId]: origPath };
    if (Object.keys(origs).length) next.__originals = origs; else delete next.__originals;
    await saveZuordnung(next);
  };

  const downloadPhoto = async (slotId) => {
    const url = signedUrls[slotId];
    if (!url) return;
    try {
      const res = await fetch(url);
      const blob = await res.blob();
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = `${(FOTO_SLOTS.find(s => s.id === slotId)?.label || slotId).replace(/\s+/g, '_')}.jpg`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(a.href);
    } catch {}
  };

  const handleFileUpload = async (e) => {
    const file = e.target.files?.[0];
    if (!file || !uploadSlot) return;
    try {
      const result = await uploadPhotoBlob(file, projektId, gutachten.id, session, workerUrl);
      await assignPhoto(uploadSlot, result.storage_path);
    } catch (err) { console.error('Foto-Upload fehlgeschlagen:', err); }
    setUploadSlot(null);
    if (fileInputRef.current) fileInputRef.current.value = '';
  };

  const triggerUpload = (slotId) => {
    setUploadSlot(slotId);
    setExpandedSlot(null);
    setTimeout(() => fileInputRef.current?.click(), 50);
  };

  return (
    <div className="card" style={{ marginBottom: 'var(--space-4)' }}>
      <div className="card-header">
        <span className="card-title">Gutachten-Fotos zuordnen</span>
      </div>
      <div style={{ padding: 'var(--space-4)' }}>
        <p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 'var(--space-3)' }}>
          Ordne Fotos den Platzhaltern im Gutachten zu. Beim DOCX-Export werden sie automatisch eingefügt.
        </p>

        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(170px, 1fr))', gap: 'var(--space-3)' }}>
          {FOTO_SLOTS.map(slot => {
            const path = zuordnung[slot.id];
            const url = signedUrls[slot.id];
            const isExpanded = expandedSlot === slot.id;
            return (
              <div key={slot.id} style={{
                border: `2px solid ${isExpanded ? 'var(--primary)' : 'var(--border)'}`,
                borderRadius: 'var(--radius-md)',
                padding: 'var(--space-2)', textAlign: 'center',
                background: isExpanded ? 'var(--bg)' : 'var(--bg-secondary)',
                transition: 'border-color 0.15s',
              }}>
                <div style={{ fontSize: 11, fontWeight: 600, marginBottom: 'var(--space-2)', color: 'var(--text-secondary)' }}>
                  {slot.label}
                </div>
                <div style={{
                  width: '100%', aspectRatio: '4/3', borderRadius: 'var(--radius-sm)',
                  overflow: 'hidden', marginBottom: 'var(--space-2)', cursor: path ? 'pointer' : 'default',
                  background: 'var(--bg-tertiary)', display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}
                  onClick={() => { if (path && url) { setViewSlot(slot.id); setExpandedSlot(null); } }}
                  title={path ? 'Klick: Vorschau' : ''}
                >
                  {path && url ? (
                    <img src={url} alt={slot.label} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                  ) : (
                    <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="1.5">
                      <rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/>
                    </svg>
                  )}
                </div>
                <div style={{ display: 'flex', gap: 4, justifyContent: 'center', flexWrap: 'wrap' }}>
                  {path ? (
                    <>
                      <button className="btn btn-ghost btn-sm" style={{ fontSize: 11 }}
                        onClick={() => setExpandedSlot(isExpanded ? null : slot.id)}>
                        {isExpanded ? 'Schließen' : 'Ändern'}
                      </button>
                      <button className="btn btn-ghost btn-sm" style={{ fontSize: 11 }}
                        onClick={() => downloadPhoto(slot.id)} title="Herunterladen">
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
                      </button>
                      <button className="btn btn-ghost btn-sm" style={{ fontSize: 11, color: 'var(--error)' }}
                        onClick={() => removePhoto(slot.id)} title="Entfernen">
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
                      </button>
                    </>
                  ) : (
                    <>
                      {ortsterminFotos.length > 0 && (
                        <button className="btn btn-ghost btn-sm" style={{ fontSize: 11 }}
                          onClick={() => setExpandedSlot(isExpanded ? null : slot.id)}>Auswählen</button>
                      )}
                      <button className="btn btn-ghost btn-sm" style={{ fontSize: 11 }}
                        onClick={() => triggerUpload(slot.id)}>Hochladen</button>
                    </>
                  )}
                </div>
              </div>
            );
          })}
        </div>

        {/* Inline-Picker: Ortstermin-Fotos zum Auswählen (erscheint unter den Slots) */}
        {expandedSlot && (
          <div style={{
            marginTop: 'var(--space-4)', padding: 'var(--space-4)',
            background: 'var(--bg-secondary)', borderRadius: 'var(--radius-md)',
            border: '1px solid var(--primary)',
          }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-3)' }}>
              <span style={{ fontWeight: 600, fontSize: 14 }}>
                Foto für &quot;{FOTO_SLOTS.find(s => s.id === expandedSlot)?.label}&quot; wählen
              </span>
              <button className="btn btn-ghost btn-sm" onClick={() => setExpandedSlot(null)}>Schließen</button>
            </div>
            {loading ? (
              <div style={{ textAlign: 'center', padding: 16, color: 'var(--text-tertiary)' }}>Lade…</div>
            ) : ortsterminFotos.length === 0 ? (
              <div style={{ textAlign: 'center', padding: 16, color: 'var(--text-secondary)' }}>
                Keine Ortstermin-Fotos vorhanden.
                <button className="btn btn-primary btn-sm" style={{ marginLeft: 12 }}
                  onClick={() => triggerUpload(expandedSlot)}>Vom Gerät hochladen</button>
              </div>
            ) : (
              <>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))', gap: 8 }}>
                  {ortsterminFotos.map(foto => (
                    <div key={foto.id}
                      onClick={() => assignPhoto(expandedSlot, foto.image_url)}
                      style={{
                        cursor: 'pointer', borderRadius: 6, overflow: 'hidden',
                        aspectRatio: '4/3', background: 'var(--bg-tertiary)',
                        border: zuordnung[expandedSlot] === foto.image_url ? '3px solid var(--primary)' : '2px solid transparent',
                        transition: 'border-color 0.15s',
                      }}
                      onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--primary)'}
                      onMouseLeave={e => {
                        e.currentTarget.style.borderColor = zuordnung[expandedSlot] === foto.image_url ? 'var(--primary)' : 'transparent';
                      }}
                    >
                      {fotoUrls[foto.id] ? (
                        <img src={fotoUrls[foto.id]} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                      ) : (
                        <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                          <div className="spinner" style={{ width: 16, height: 16 }} />
                        </div>
                      )}
                    </div>
                  ))}
                </div>
                <div style={{ marginTop: 12 }}>
                  <button className="btn btn-ghost btn-sm" style={{ fontSize: 12 }}
                    onClick={() => triggerUpload(expandedSlot)}>Stattdessen vom Gerät hochladen</button>
                </div>
              </>
            )}
          </div>
        )}

        {/* Inline-Vorschau: zugeordnetes Foto groß anzeigen */}
        {viewSlot && signedUrls[viewSlot] && (() => {
          const slotData = FOTO_SLOTS.find(s => s.id === viewSlot);
          return (
            <div style={{
              marginTop: 'var(--space-4)', padding: 'var(--space-4)',
              background: 'var(--bg-secondary)', borderRadius: 'var(--radius-md)',
              border: '1px solid var(--border)',
            }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-3)' }}>
                <span style={{ fontWeight: 600, fontSize: 14 }}>{slotData?.label}</span>
                <div style={{ display: 'flex', gap: 8 }}>
                  <button className="btn btn-ghost btn-sm" style={{ fontSize: 12, color: 'var(--vl-blue)', fontWeight: 600 }}
                    onClick={() => setEditorSlot(viewSlot)}>Bearbeiten</button>
                  <button className="btn btn-ghost btn-sm" style={{ fontSize: 12 }}
                    onClick={() => { setViewSlot(null); setExpandedSlot(viewSlot); }}>Ändern</button>
                  {zuordnung.__originals?.[viewSlot] && (
                    <button className="btn btn-ghost btn-sm" style={{ fontSize: 12, color: 'var(--vl-blue)' }}
                      title="Slot auf das komplett unbearbeitete Originalbild zurücksetzen"
                      onClick={() => { restoreOriginal(viewSlot); }}>↺ Original</button>
                  )}
                  <button className="btn btn-ghost btn-sm" style={{ fontSize: 12 }}
                    onClick={() => downloadPhoto(viewSlot)}>Herunterladen</button>
                  <button className="btn btn-ghost btn-sm" style={{ fontSize: 12, color: 'var(--error)' }}
                    onClick={() => { removePhoto(viewSlot); setViewSlot(null); }}>Löschen</button>
                  <button className="btn btn-ghost btn-sm" onClick={() => setViewSlot(null)}>Schließen</button>
                </div>
              </div>
              <div style={{ textAlign: 'center' }}>
                <img src={signedUrls[viewSlot]} alt={slotData?.label || ''}
                  style={{ maxWidth: '100%', maxHeight: '60vh', objectFit: 'contain', borderRadius: 8 }} />
              </div>
            </div>
          );
        })()}

        <input ref={fileInputRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={handleFileUpload} />

        {editorSlot && (originalUrls[editorSlot] || signedUrls[editorSlot]) && (
          <FotoEditor
            imageUrl={originalUrls[editorSlot] || signedUrls[editorSlot]}
            projektId={projektId}
            gutachtenId={gutachten.id}
            session={session}
            workerUrl={workerUrl}
            title={FOTO_SLOTS.find(s => s.id === editorSlot)?.label || 'Bild'}
            onClose={() => setEditorSlot(null)}
            onSaved={async (newPath) => {
              const slot = editorSlot;
              setEditorSlot(null);
              setViewSlot(null);
              // Beim ersten Bearbeiten das echte, unbearbeitete Original merken (danach unverändert lassen)
              const origs = { ...(zuordnung.__originals || {}) };
              if (!origs[slot] && zuordnung[slot]) origs[slot] = zuordnung[slot];
              await saveZuordnung({ ...zuordnung, [slot]: newPath, __originals: origs });
            }}
          />
        )}
      </div>
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// FotosGrid — Alle Fotos als Grid mit Lightbox + Download
// ────────────────────────────────────────────────────────────────────

// Lightbox: Vollbild-Viewer mit Prev/Next und Download
const FotoLightbox = ({ photos, startIndex, session, onClose, onDelete, projektId, workerUrl, gutachtenId, onChanged }) => {
  const [idx, setIdx] = useState(startIndex);
  const [url, setUrl] = useState(null);
  const [loading, setLoading] = useState(true);
  const [downloading, setDownloading] = useState(false);
  const [deleting, setDeleting] = useState(false);
  const [editing, setEditing] = useState(false);
  const photo = photos[idx];

  // URL laden wenn sich idx ändert
  useEffect(() => {
    if (!photo?.image_url) return;
    let active = true;
    setLoading(true);
    setUrl(null);
    (async () => {
      const sb = await initSupabase();
      const { data } = await sb.storage.from('ortstermin-photos').createSignedUrl(photo.image_url, 3600);
      if (active && data?.signedUrl) { setUrl(data.signedUrl); setLoading(false); }
    })();
    return () => { active = false; };
  }, [photo?.image_url]);

  // Tastatur: Esc schließen, Pfeile navigieren (ruht, während der Editor offen ist)
  useEffect(() => {
    const handle = (e) => {
      if (editing) return;
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowLeft' && idx > 0) setIdx(i => i - 1);
      if (e.key === 'ArrowRight' && idx < photos.length - 1) setIdx(i => i + 1);
    };
    document.addEventListener('keydown', handle);
    return () => document.removeEventListener('keydown', handle);
  }, [idx, photos.length, onClose, editing]);

  const handleDownload = async () => {
    if (!url || downloading) return;
    setDownloading(true);
    try {
      const res = await fetch(url);
      const blob = await res.blob();
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      const ext = photo.image_url.split('.').pop() || 'jpg';
      const kapLabel = photo.kapitel || 'foto';
      a.download = `${kapLabel}_${new Date(photo.created_at).toISOString().slice(0,10)}_${photo.id.slice(0,6)}.${ext}`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(a.href);
    } catch (err) {
      alert('Download fehlgeschlagen: ' + (err.message || err));
    } finally {
      setDownloading(false);
    }
  };

  const handleDelete = async () => {
    if (!photo || deleting) return;
    if (!confirm('Foto wirklich löschen?')) return;
    setDeleting(true);
    try {
      // Storage-Datei löschen
      if (photo.image_url) {
        const sb = await initSupabase();
        await sb.storage.from('ortstermin-photos').remove([photo.image_url]);
      }
      // Note-Record löschen
      await deleteNote(photo.id);
      if (onDelete) onDelete(photo.id);
      // Navigation nach Löschung
      if (photos.length <= 1) {
        onClose();
      } else if (idx >= photos.length - 1) {
        setIdx(i => i - 1);
      }
    } catch (err) {
      alert('Löschen fehlgeschlagen: ' + (err.message || err));
    } finally {
      setDeleting(false);
    }
  };

  const kapLabel = photo?.kapitel
    ? (VL_KAPITEL_TAGS.find(k => k.id === photo.kapitel) || {}).label || photo.kapitel
    : null;

  const navBtn = {
    position: 'absolute', top: '50%', transform: 'translateY(-50%)',
    width: 44, height: 44, borderRadius: '50%',
    background: 'rgba(0,0,0,0.5)', border: 'none', cursor: 'pointer',
    display: 'flex', alignItems: 'center', justifyContent: 'center',
    color: 'white', fontSize: 22, zIndex: 10,
  };

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 10000,
      background: 'rgba(0,0,0,0.9)',
      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
    }}>
      {/* Top bar */}
      <div onClick={e => e.stopPropagation()} style={{
        position: 'absolute', top: 0, left: 0, right: 0,
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        padding: '12px 16px', zIndex: 10,
      }}>
        <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: 13 }}>
          {kapLabel && <span style={{ marginRight: 8 }}>{kapLabel}</span>}
          {idx + 1} / {photos.length}
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          {projektId && workerUrl && (
            <button onClick={() => setEditing(true)} disabled={!url}
              style={{
                background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: 'var(--radius-sm)',
                color: 'white', cursor: 'pointer', padding: '8px 14px', fontSize: 13,
                display: 'flex', alignItems: 'center', gap: 6, opacity: !url ? 0.5 : 1,
              }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/>
              </svg>
              Bearbeiten
            </button>
          )}
          <button onClick={handleDownload} disabled={downloading || !url}
            style={{
              background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: 'var(--radius-sm)',
              color: 'white', cursor: 'pointer', padding: '8px 14px', fontSize: 13,
              display: 'flex', alignItems: 'center', gap: 6, opacity: downloading ? 0.5 : 1,
            }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
            </svg>
            {downloading ? 'Lade...' : 'Herunterladen'}
          </button>
          <button onClick={handleDelete} disabled={deleting}
            style={{
              background: 'rgba(239,68,68,0.2)', border: 'none', borderRadius: 'var(--radius-sm)',
              color: '#FCA5A5', cursor: 'pointer', padding: '8px 14px', fontSize: 13,
              display: 'flex', alignItems: 'center', gap: 6, opacity: deleting ? 0.5 : 1,
            }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
            </svg>
            {deleting ? 'Lösche...' : 'Löschen'}
          </button>
          <button onClick={onClose}
            style={{
              background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: 'var(--radius-sm)',
              color: 'white', cursor: 'pointer', padding: '8px 12px', fontSize: 18, lineHeight: 1,
            }}>
            ×
          </button>
        </div>
      </div>

      {/* Bild */}
      <div onClick={e => e.stopPropagation()} style={{
        position: 'relative', width: '100%', height: '100%',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: '60px 16px 16px',
      }}>
        {loading ? (
          <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: 14 }}>Lade...</div>
        ) : (
          <img src={url} alt=""
            style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 4 }} />
        )}

        {/* Prev/Next */}
        {idx > 0 && (
          <button onClick={() => setIdx(i => i - 1)} style={{ ...navBtn, left: 12 }}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="15 18 9 12 15 6"/></svg>
          </button>
        )}
        {idx < photos.length - 1 && (
          <button onClick={() => setIdx(i => i + 1)} style={{ ...navBtn, right: 12 }}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="9 18 15 12 9 6"/></svg>
          </button>
        )}
      </div>

      {editing && url && (
        <FotoEditor
          imageUrl={url}
          z={10001}
          projektId={photo.project_id || projektId}
          gutachtenId={photo.gutachten_id || gutachtenId}
          session={session}
          workerUrl={workerUrl}
          title="Foto"
          onClose={() => setEditing(false)}
          onSaved={async (newPath) => {
            try {
              await insertNote({
                project_id: photo.project_id || projektId,
                gutachten_id: photo.gutachten_id || gutachtenId,
                objekt_id: photo.objekt_id || null,
                kapitel: photo.kapitel || null,
                type: 'photo',
                image_url: newPath,
                user_id: null,
                sort_order: (photos.length || 0) + 1,
              });
            } catch (e) { alert('Speichern fehlgeschlagen: ' + (e.message || e)); }
            setEditing(false);
            if (onChanged) onChanged();
            onClose();
          }}
        />
      )}
    </div>
  );
};

const FotoThumbnail = ({ note, session, onOpen }) => {
  const [url, setUrl] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (!note.image_url) return;
    let active = true;
    (async () => {
      const sb = await initSupabase();
      const { data } = await sb.storage.from('ortstermin-photos').createSignedUrl(note.image_url, 3600);
      if (active && data?.signedUrl) { setUrl(data.signedUrl); setLoading(false); }
    })();
    return () => { active = false; };
  }, [note.image_url]);

  return (
    <div onClick={() => !loading && onOpen()} style={{
      position: 'relative', borderRadius: 'var(--radius-md)', overflow: 'hidden',
      border: '1px solid var(--border-light)', background: 'var(--surface-light)',
      aspectRatio: '4/3', cursor: 'pointer',
    }}>
      {loading ? (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--text-tertiary)', fontSize: 12 }}>
          Lade...
        </div>
      ) : (
        <img src={url} alt=""
          style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
      )}
      {note.kapitel && (
        <span style={{
          position: 'absolute', top: 6, left: 6,
          background: 'rgba(0,0,0,0.55)', color: 'white',
          fontSize: 10, fontWeight: 600, padding: '2px 6px', borderRadius: 4,
        }}>
          {(VL_KAPITEL_TAGS.find(k => k.id === note.kapitel) || {}).label || note.kapitel}
        </span>
      )}
    </div>
  );
};

const FotosGrid = ({ notes: notesProp, gutachtenId, session, projektId, workerUrl }) => {
  // Zwei Modi: (A) notes als Prop (aus OrtsterminTab), (B) selbst laden (aus Dashboard)
  const [loadedNotes, setLoadedNotes] = useState([]);
  const [loading, setLoading] = useState(false);
  const [lightboxIdx, setLightboxIdx] = useState(null); // null = geschlossen
  const [deleteTick, setDeleteTick] = useState(0); // Re-Fetch nach Löschung

  useEffect(() => {
    if (notesProp || !gutachtenId) return;
    let active = true;
    setLoading(true);
    ladeNotes(gutachtenId).then(rows => {
      if (active) { setLoadedNotes(rows); setLoading(false); }
    }).catch(() => { if (active) setLoading(false); });
    return () => { active = false; };
  }, [gutachtenId, notesProp, deleteTick]);

  const allNotes = notesProp || loadedNotes;
  const photos = useMemo(() => allNotes.filter(n => n.type === 'photo' && n.image_url), [allNotes]);

  // Nach Kapitel gruppieren
  const grouped = useMemo(() => {
    const map = {};
    photos.forEach(p => {
      const key = p.kapitel || '_none';
      if (!map[key]) map[key] = [];
      map[key].push(p);
    });
    return map;
  }, [photos]);

  const kapitelOrder = VL_KAPITEL_TAGS.map(k => k.id).filter(id => grouped[id]);
  if (grouped['_none']) kapitelOrder.push('_none');

  const handleDownloadAll = async () => {
    for (const photo of photos) {
      const sb = await initSupabase();
      const { data } = await sb.storage.from('ortstermin-photos').createSignedUrl(photo.image_url, 3600);
      if (!data?.signedUrl) continue;
      try {
        const res = await fetch(data.signedUrl);
        const blob = await res.blob();
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        const ext = photo.image_url.split('.').pop() || 'jpg';
        const kapLabel = photo.kapitel || 'foto';
        a.download = `${kapLabel}_${new Date(photo.created_at).toISOString().slice(0,10)}_${photo.id.slice(0,6)}.${ext}`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(a.href);
      } catch {}
      await new Promise(r => setTimeout(r, 300)); // kurze Pause zwischen Downloads
    }
  };

  return (
    <div className="card">
      <div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <span className="card-title">Fotos ({photos.length})</span>
        {photos.length > 0 && (
          <button className="btn btn-ghost btn-sm" onClick={handleDownloadAll}
            style={{ fontSize: 12, display: 'inline-flex', alignItems: 'center', gap: 6 }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
            </svg>
            Alle herunterladen
          </button>
        )}
      </div>
      <div style={{ padding: 'var(--space-4)' }}>
        {loading ? (
          <div style={{ textAlign: 'center', color: 'var(--text-tertiary)', padding: 'var(--space-5)', fontSize: 14 }}>Lade Fotos…</div>
        ) : photos.length === 0 ? (
          <div style={{ textAlign: 'center', color: 'var(--text-tertiary)', padding: 'var(--space-5)', fontSize: 14 }}>
            Noch keine Fotos. Fotos werden unter "Aufnahme" aufgenommen.
          </div>
        ) : (
          kapitelOrder.map(kapId => {
            const kapLabel = kapId === '_none'
              ? 'Ohne Zuordnung'
              : (VL_KAPITEL_TAGS.find(k => k.id === kapId) || {}).label || kapId;
            return (
              <div key={kapId} style={{ marginBottom: 'var(--space-4)' }}>
                {kapitelOrder.length > 1 && (
                  <div style={{
                    fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
                    letterSpacing: '0.08em', color: 'var(--text-secondary)',
                    marginBottom: 'var(--space-2)',
                  }}>
                    {kapLabel} ({grouped[kapId].length})
                  </div>
                )}
                <div style={{
                  display: 'grid',
                  gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
                  gap: 'var(--space-3)',
                }}>
                  {grouped[kapId].map(n => {
                    const flatIdx = photos.indexOf(n);
                    return <FotoThumbnail key={n.id} note={n} session={session} onOpen={() => setLightboxIdx(flatIdx)} />;
                  })}
                </div>
              </div>
            );
          })
        )}
      </div>
      {lightboxIdx !== null && (
        <FotoLightbox
          photos={photos}
          startIndex={lightboxIdx}
          session={session}
          projektId={projektId}
          workerUrl={workerUrl}
          gutachtenId={gutachtenId}
          onChanged={() => setDeleteTick(t => t + 1)}
          onClose={() => setLightboxIdx(null)}
          onDelete={(deletedId) => {
            // Bei notesProp: lokales Filtern (Parent muss refreshen)
            // Bei selbst geladenem: Re-Fetch triggern
            if (notesProp) {
              // FotosGrid wird vom Parent neu gerendert wenn notes sich ändern
            }
            setDeleteTick(t => t + 1);
          }}
        />
      )}
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// Live-Recording-Overlay während Einzel-Notiz oder Rundgang
// ────────────────────────────────────────────────────────────────────
// Chip-Style für noch offene Bereiche im Live-Nudge
const chipStyleOffen = {
  display: 'inline-block',
  padding: '2px 8px',
  marginRight: 5,
  marginBottom: 4,
  borderRadius: 99,
  background: 'rgba(255,255,255,0.12)',
  color: 'rgba(255,255,255,0.92)',
  fontSize: 11,
  whiteSpace: 'nowrap',
};

const RecordingOverlay = ({ voice, rundgang, mode, onStop, kontextGutachten, objekte, bereicheStatus }) => {
  const [elapsed, setElapsed] = useState(0);

  useEffect(() => {
    if (!voice.isRec) return;
    const interval = setInterval(() => {
      setElapsed((Date.now() - (rundgang.getCurrentAudioPositionMs() ? Date.now() - rundgang.getCurrentAudioPositionMs() : Date.now())) / 1000 | 0);
    }, 1000);
    return () => clearInterval(interval);
  }, [voice.isRec, rundgang]);

  // Forward live text to Rundgang
  useEffect(() => {
    if (mode === 'rundgang') {
      rundgang.onLiveText(voice.text);
    }
  }, [voice.text, mode, rundgang]);

  const currentKapitel = rundgang.currentTagId
    ? VL_KAPITEL_TAGS.find(k => k.id === rundgang.currentTagId)
    : null;
  const currentObjekt = rundgang.currentObjektId
    ? objekte.find(o => o.id === rundgang.currentObjektId)
    : null;

  // ── Live-Nudge: welche Pflicht-Bereiche sind noch offen? ──
  // Soll = bereicheStatus (aus Profil). Erfasst = bereits gespeicherte Notes
  // (b.done) ODER während dieser Aufnahme schon besuchte Live-Segmente.
  const offeneBereiche = useMemo(() => {
    if (mode !== 'rundgang' || !bereicheStatus || bereicheStatus.length === 0) return [];
    const liveTagIds = new Set((rundgang.segments || []).map(s => s.tagId).filter(Boolean));
    // aktueller Bereich gilt auch als "begonnen"
    if (rundgang.currentTagId) liveTagIds.add(rundgang.currentTagId);
    // Nur Bereiche zeigen, die der Detector erkennen kann — sonst blieben sie
    // dauerhaft offen und der Nudge würde unzuverlässig wirken.
    return bereicheStatus.filter(b =>
      !b.done && !liveTagIds.has(b.id) && RUNDGANG_DETECTABLE_TAGS.has(b.id)
    );
  }, [mode, bereicheStatus, rundgang.segments, rundgang.currentTagId]);

  // Bei ETW nach Eigentum gruppieren (GE/SE getrennt anzeigen)
  const offeneGE = offeneBereiche.filter(b => b.eigentum === 'ge');
  const offeneSE = offeneBereiche.filter(b => b.eigentum === 'se');
  const offeneRest = offeneBereiche.filter(b => !b.eigentum);
  const hatEigentumsTrennung = offeneBereiche.some(b => b.eigentum);

  return (
    <div style={{
      position: 'fixed',
      bottom: 0, left: 0, right: 0,
      background: 'var(--vl-blue-dark, #0A2540)',
      color: 'white',
      padding: 'var(--space-4) var(--space-5)',
      boxShadow: '0 -8px 24px rgba(0,0,0,0.3)',
      zIndex: 100,
      borderTop: '3px solid var(--vl-orange)',
    }}>
      <div style={{
        maxWidth: 1200, margin: '0 auto',
        display: 'grid', gridTemplateColumns: 'auto 1fr auto', gap: 'var(--space-4)',
        alignItems: 'center',
      }}>
        <div>
          <div style={{
            width: 48, height: 48, borderRadius: 24,
            background: '#DC2626', display: 'flex', alignItems: 'center', justifyContent: 'center',
            animation: 'pulse 1.5s infinite',
          }}>
            <div style={{ width: 16, height: 16, borderRadius: 8, background: 'white' }} />
          </div>
        </div>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 12, opacity: 0.7, marginBottom: 2 }}>
            Aufnahme läuft
            {currentKapitel && <span> · {currentKapitel.icon} {currentKapitel.label}</span>}
            {currentObjekt && <span> · Objekt: {currentObjekt.bezeichnung}</span>}
          </div>
          <div style={{
            fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          }}>
            {voice.text || voice.interim || <em style={{ opacity: 0.5 }}>Hört zu…</em>}
          </div>
        </div>
        <button
          onClick={onStop}
          className="btn"
          style={{
            background: '#DC2626', color: 'white', fontWeight: 600,
            padding: '10px 18px', fontSize: 14, border: 'none',
            borderRadius: 'var(--radius-md)', cursor: 'pointer',
          }}
        >
          Stop
        </button>
      </div>

      {/* Live-Nudge: noch offene Bereiche (nur Rundgang) */}
      {mode === 'rundgang' && offeneBereiche.length > 0 && (
        <div style={{
          maxWidth: 1200, margin: 'var(--space-3) auto 0',
          paddingTop: 'var(--space-3)', borderTop: '1px solid rgba(255,255,255,0.12)',
          fontSize: 12,
        }}>
          {hatEigentumsTrennung ? (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px 16px', alignItems: 'baseline' }}>
              {offeneGE.length > 0 && (
                <span>
                  <span style={{ opacity: 0.6, marginRight: 6 }}>Gemeinschaftseigentum offen:</span>
                  {offeneGE.map(b => (
                    <span key={b.id} style={chipStyleOffen}>{b.label}</span>
                  ))}
                </span>
              )}
              {offeneSE.length > 0 && (
                <span>
                  <span style={{ opacity: 0.6, marginRight: 6 }}>Sondereigentum offen:</span>
                  {offeneSE.map(b => (
                    <span key={b.id} style={chipStyleOffen}>{b.label}</span>
                  ))}
                </span>
              )}
              {offeneRest.length > 0 && offeneRest.map(b => (
                <span key={b.id} style={chipStyleOffen}>{b.label}</span>
              ))}
            </div>
          ) : (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
              <span style={{ opacity: 0.6, marginRight: 2 }}>Noch offen:</span>
              {offeneBereiche.map(b => (
                <span key={b.id} style={chipStyleOffen}>{b.label}</span>
              ))}
            </div>
          )}
        </div>
      )}
      {mode === 'rundgang' && offeneBereiche.length === 0 && bereicheStatus && bereicheStatus.length > 0 && (
        <div style={{
          maxWidth: 1200, margin: 'var(--space-3) auto 0',
          paddingTop: 'var(--space-3)', borderTop: '1px solid rgba(255,255,255,0.12)',
          fontSize: 12, color: '#4ade80',
        }}>
          <span style={{ color: '#4ade80' }}>✓ Alle Pflicht-Bereiche erfasst</span>
        </div>
      )}
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// Rundgang-Ergebnis-Review (nach Stop, vor Speichern)
// ────────────────────────────────────────────────────────────────────
const RundgangResult = ({ splitResult, objekte, onAccept, onDiscard }) => {
  const [edited, setEdited] = useState(() => splitResult.segments.map(s => ({ ...s })));

  const updateSegment = (idx, patch) => {
    setEdited(prev => prev.map((s, i) => i === idx ? { ...s, ...patch } : s));
  };

  const removeSegment = (idx) => {
    setEdited(prev => prev.filter((_, i) => i !== idx));
  };

  return (
    <div style={{ padding: 'var(--space-4)' }}>
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        marginBottom: 'var(--space-4)',
      }}>
        <div>
          <h3 style={{ fontSize: 18, fontWeight: 600, marginBottom: 4 }}>
            Rundgang-Ergebnis prüfen
          </h3>
          <div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
            {edited.length} Segment{edited.length !== 1 ? 'e' : ''} erkannt.
            Prüfe Inhalt und Zuordnung, korrigiere wenn nötig.
          </div>
        </div>
      </div>

      {splitResult.todos && splitResult.todos.length > 0 && (
        <div style={{
          padding: 'var(--space-3)', marginBottom: 'var(--space-4)',
          background: 'var(--warning-bg, #FEF3C7)',
          border: '1px solid var(--warning, #F59E0B)',
          borderRadius: 'var(--radius-md)',
        }}>
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--warning)', marginBottom: 4 }}>
            {splitResult.todos.length} offene Punkt{splitResult.todos.length !== 1 ? 'e' : ''} erkannt
          </div>
          {splitResult.todos.map((t, i) => (
            <div key={i} style={{ fontSize: 13, marginTop: 2 }}>→ {t.text}</div>
          ))}
        </div>
      )}

      {edited.map((seg, idx) => {
        const kapitel = VL_KAPITEL_TAGS.find(k => k.id === seg.tagId);
        return (
          <div key={idx} style={{
            padding: 'var(--space-3)',
            background: 'var(--surface)',
            border: '1px solid var(--border-light)',
            borderRadius: 'var(--radius-md)',
            marginBottom: 'var(--space-3)',
          }}>
            <div style={{
              display: 'flex', gap: 'var(--space-2)', alignItems: 'center',
              marginBottom: 'var(--space-2)', flexWrap: 'wrap',
            }}>
              <select
                value={seg.tagId || 'aussen'}
                onChange={e => updateSegment(idx, { tagId: e.target.value })}
                style={{
                  padding: '4px 8px', fontSize: 12,
                  border: '1px solid var(--border-light)',
                  borderRadius: 'var(--radius-sm)',
                }}
              >
                {VL_KAPITEL_TAGS.map(k => (
                  <option key={k.id} value={k.id}>{k.label}</option>
                ))}
              </select>

              {objekte.length > 1 && (
                <select
                  value={seg.objektId || ''}
                  onChange={e => updateSegment(idx, { objektId: e.target.value || null })}
                  style={{
                    padding: '4px 8px', fontSize: 12,
                    border: '1px solid var(--border-light)',
                    borderRadius: 'var(--radius-sm)',
                  }}
                >
                  <option value="">— kein Objekt —</option>
                  {objekte.map(o => (
                    <option key={o.id} value={o.id}>{o.bezeichnung}</option>
                  ))}
                </select>
              )}

              {seg.isBeurteilung && (
                <Pill variant="orange" style={{ fontSize: 10 }}>Beurteilung</Pill>
              )}

              <button
                onClick={() => removeSegment(idx)}
                style={{
                  marginLeft: 'auto', background: 'none', border: 'none',
                  color: 'var(--danger)', cursor: 'pointer', fontSize: 12,
                }}
              >
                Verwerfen
              </button>
            </div>
            <textarea
              value={seg.text || ''}
              onChange={e => updateSegment(idx, { text: e.target.value })}
              style={{
                width: '100%', minHeight: 80, padding: 'var(--space-2)',
                border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
                fontFamily: 'inherit', fontSize: 13, resize: 'vertical',
                background: 'var(--surface-light)',
              }}
            />
          </div>
        );
      })}

      <div style={{
        display: 'flex', gap: 'var(--space-3)', marginTop: 'var(--space-4)',
        justifyContent: 'flex-end',
      }}>
        <button className="btn btn-ghost" onClick={onDiscard}>
          Alles verwerfen
        </button>
        <button
          className="btn btn-primary"
          onClick={() => onAccept(edited)}
          disabled={edited.length === 0}
        >
          {edited.length} Notiz{edited.length !== 1 ? 'en' : ''} speichern
        </button>
      </div>
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// Phase 5c — TranscriptImportModal
// Modal zum Einfügen eines bereits transkribierten OT-Texts.
// Geht durch dieselbe split-rundgang-Pipeline wie Audio-Aufnahmen.
// Nutzer sieht danach die bekannte Review-UI.
// ────────────────────────────────────────────────────────────────────
const TranscriptImportModal = ({ isMobile, onCancel, onImport }) => {
  const [text, setText] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const charCount = text.length;
  const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
  const tooShort = charCount > 0 && charCount < 50;
  const tooLong = charCount > 60000;
  const canSubmit = charCount >= 50 && !tooLong && !submitting;

  const handleSubmit = async () => {
    if (!canSubmit) return;
    setSubmitting(true);
    try {
      await onImport(text);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={submitting ? undefined : onCancel}>
      <div
        className="modal"
        onClick={e => e.stopPropagation()}
        style={{ maxWidth: 720, width: isMobile ? '100%' : '90%' }}
      >
        <div className="modal-header">
          <div>
            <div className="modal-title">Transkript importieren</div>
            <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 }}>
              Bereits transkribierten OT-Text einfügen — wird automatisch in Notizen pro Bereich (Keller, EG, DG, …) aufgeteilt.
            </div>
          </div>
          <button className="modal-close" onClick={onCancel} disabled={submitting}>×</button>
        </div>
        <div className="modal-body" style={{ padding: 'var(--space-4) var(--space-5)' }}>
          <div style={{
            background: 'var(--info-bg, #EFF6FF)',
            border: '1px solid var(--info-border, #BFDBFE)',
            borderRadius: 'var(--radius-md)',
            padding: 'var(--space-3)',
            fontSize: 13,
            color: 'var(--text-secondary)',
            marginBottom: 'var(--space-3)',
            lineHeight: 1.5,
          }}>
            <strong>Tipp:</strong> Wenn du das Transkript bereits als <em>einzelne Notiz</em> erfasst hast,
            lösche sie zuerst — sonst wird der Text doppelt einsortiert.
            <br/>
            Nach dem Import siehst du eine Vorschau der zugeordneten Bereiche und kannst sie noch anpassen.
          </div>
          <textarea
            value={text}
            onChange={e => setText(e.target.value)}
            disabled={submitting}
            placeholder="Hier den vollständigen Transkript-Text einfügen (Strg+V / Cmd+V)..."
            spellCheck={false}
            style={{
              width: '100%',
              minHeight: isMobile ? 220 : 320,
              padding: 'var(--space-3)',
              fontSize: 14,
              fontFamily: 'inherit',
              lineHeight: 1.5,
              border: '1px solid var(--border-light)',
              borderRadius: 'var(--radius-md)',
              resize: 'vertical',
              boxSizing: 'border-box',
            }}
          />
          <div style={{
            marginTop: 8,
            fontSize: 12,
            color: tooShort || tooLong ? 'var(--danger)' : 'var(--text-tertiary)',
            display: 'flex', justifyContent: 'space-between',
          }}>
            <span>
              {wordCount.toLocaleString('de-DE')} Wörter · {charCount.toLocaleString('de-DE')} Zeichen
            </span>
            <span>
              {tooShort && 'Mindestens 50 Zeichen'}
              {tooLong && 'Maximal 60.000 Zeichen'}
              {!tooShort && !tooLong && charCount > 0 && (charCount < 1000 ? 'kurz' : charCount < 10000 ? 'normal' : 'umfangreich')}
            </span>
          </div>
        </div>
        <div className="modal-footer" style={{
          display: 'flex', gap: 'var(--space-2)', justifyContent: 'flex-end',
          padding: 'var(--space-3) var(--space-5)',
          borderTop: '1px solid var(--border-light)',
        }}>
          <button className="btn btn-ghost" onClick={onCancel} disabled={submitting}>
            Abbrechen
          </button>
          <button
            className="btn btn-primary"
            onClick={handleSubmit}
            disabled={!canSubmit}
            style={{ minWidth: 200, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}
          >
            {submitting ? (
              <>
                <span style={{
                  width: 14, height: 14, border: '2px solid currentColor',
                  borderTopColor: 'transparent', borderRadius: '50%',
                  animation: 'spin 0.8s linear infinite',
                }} />
                Verarbeite...
              </>
            ) : (
              <>Vorschau & Import</>
            )}
          </button>
        </div>
      </div>
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// Haupt-OrtsterminTab: Split-Screen mit Notes + Dokument
// ────────────────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════
// ORTSTERMIN-VORBEREITUNG — Checkliste vor dem Rundgang
// Der SV sieht auf einen Blick: Wo? Wann? Wer lässt mich rein?
// Was weiß ich schon? Was fehlt? Welche Räume muss ich durchgehen?
// ══════════════════════════════════════════════════════════════════
const OrtsterminVorbereitung = ({ p, g, notes, alwaysExpanded, hideBereiche, defaultCollapsed }) => {
  const [collapsed, setCollapsed] = useState(!!defaultCollapsed);
  const objekte = g.objekte || [];
  const obj = objekte[0] || {};

  // ── 1. Kontaktperson für Zugang ──
  const kontakte = useMemo(() => {
    const rollen = ['antragsgegner', 'eigentuemer', 'verwalter', 'mieter', 'ansprechpartner'];
    return (p.beteiligte || [])
      .filter(b => rollen.includes((b.rolle || '').toLowerCase()))
      .map(b => ({
        rolle: b.rolle,
        name: b.name,
        anschrift: b.anschrift,
      }));
  }, [p.beteiligte]);

  // ── 2. Rundgang-Bereiche mit Status (zentrale Profil-Funktion) ──
  const rundgangProfil = useMemo(() => getRundgangBereiche(obj), [obj]);
  const bereicheStatus = useMemo(() => {
    return rundgangProfil.pflicht.map(b => ({
      id: b.id,
      label: b.label,
      eigentum: b.eigentum,
      done: (notes || []).some(n => n.kapitel === b.id),
    }));
  }, [rundgangProfil, notes]);
  const objektHinweise = rundgangProfil.hinweise || [];

  const bereicheDone = bereicheStatus.filter(b => b.done).length;
  const bereicheTotal = bereicheStatus.length;

  // ── 3. Fehlende Stammdaten (vor Ort klärbar) ──
  const fehlend = useMemo(() => {
    const items = [];
    const v = (val) => val != null && val !== '' && val !== 0;

    // Gebäude
    if (!v(obj.objekttyp)) items.push({ label: 'Objekttyp', hint: 'Vor Ort feststellen (EFH/MFH/ETW etc.)', prio: 'hoch' });
    if (!v(obj.baujahr)) items.push({ label: 'Baujahr', hint: 'Am Gebäude oder bei Eigentümer erfragen', prio: 'hoch' });
    if (!v(obj.wohnflaeche)) items.push({ label: 'Wohn-/Nutzfläche', hint: 'Nachmessen oder aus Bauunterlagen', prio: 'hoch' });

    // Grundstück
    if (!v(obj.groesse_qm)) items.push({ label: 'Grundstücksgröße', hint: 'Aus Grundbuchauszug oder nachmessen', prio: 'mittel' });
    if (!v(obj.gemarkung)) items.push({ label: 'Gemarkung', hint: 'Grundbuchauszug hochladen', prio: 'niedrig' });
    if (!v(obj.flurstueck)) items.push({ label: 'Flurstück', hint: 'Grundbuchauszug hochladen', prio: 'niedrig' });

    // Planung
    if (!v(obj.bodenrichtwert)) items.push({ label: 'Bodenrichtwert', hint: 'Bodenrichtwertkarte hochladen', prio: 'mittel' });
    if (!v(obj.art_bauliche_nutzung)) items.push({ label: 'Art der baulichen Nutzung', hint: 'Bebauungsplan hochladen', prio: 'niedrig' });

    // Energie
    if (!v(obj.endenergie)) items.push({ label: 'Energiekennwert', hint: 'Energieausweis beim Eigentümer anfordern', prio: 'mittel' });

    return items;
  }, [obj]);

  // ── 4. Fehlende Unterlagen ──
  const fehlendeUnterlagen = useMemo(() => {
    const uploaded = new Set((p.dokumente || []).map(d => d.typ_raw));
    const auftragsart = (p.auftragsart || '').toLowerCase();
    const objekttyp = (obj.objekttyp || '').toLowerCase();

    const expected = [
      { id: 'grundbuchauszug', label: 'Grundbuchauszug', required: true },
      { id: 'energieausweis', label: 'Energieausweis', required: true, askOwner: true },
      { id: 'bebauungsplan', label: 'Bebauungsplan', required: true },
      { id: 'bodenrichtwert', label: 'Bodenrichtwertkarte', required: true },
      { id: 'bauplan', label: 'Grundrisse / Baupläne', required: false, askOwner: true },
      { id: 'teilungserklaerung', label: 'Teilungserklärung', required: ['etw', 'mfh'].includes(objekttyp), askOwner: true },
      { id: 'altlastenauskunft', label: 'Altlastenauskunft', required: false },
      { id: 'baulastenauskunft', label: 'Baulastenauskunft', required: false },
    ];

    return expected
      .filter(e => !uploaded.has(e.id))
      .filter(e => e.required || (auftragsart === 'gericht'));
  }, [p.dokumente, p.auftragsart, obj.objekttyp]);


  if (collapsed && !alwaysExpanded) {
    // Bewusst KEINE Detail-Zusammenfassung hier (Stammdaten/Unterlagen).
    // Im Aufnahme-Moment ist das Lärm — Details gibt es beim Aufklappen.
    return (
      <div
        className="card"
        style={{ marginBottom: 'var(--space-4)', cursor: 'pointer', padding: 'var(--space-3) var(--space-4)' }}
        onClick={() => setCollapsed(false)}
      >
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
          <span style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-secondary)' }}>
            Vorbereitung
          </span>
          <span style={{ fontSize: 13, color: 'var(--vl-blue)', fontWeight: 600 }}>Anzeigen ▾</span>
        </div>
      </div>
    );
  }

  return (
    <div className="card" style={{ marginBottom: 'var(--space-4)' }}>
      {!alwaysExpanded && (
        <div className="card-header" style={{ cursor: 'pointer' }} onClick={() => setCollapsed(true)}>
          <span className="card-title">Vorbereitung</span>
          <span style={{ fontSize: 11, color: 'var(--text-tertiary)', cursor: 'pointer' }}>▲ einklappen</span>
        </div>
      )}

      <div style={{ padding: 'var(--space-4)' }}>

        {/* ── Objekt + Adresse ── */}
        <div style={{
          background: 'var(--surface-light)', borderRadius: 'var(--radius-md)',
          padding: 'var(--space-3) var(--space-4)', marginBottom: 'var(--space-4)',
        }}>
          <div style={{ fontSize: 16, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 4 }}>
            {g.adresse || 'Adresse fehlt'}
          </div>
          <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
            {obj.objekttyp ? formatFieldValue('objekttyp', obj.objekttyp) : 'Objekttyp unbekannt'}
            {obj.wohnflaeche ? ` · ${obj.wohnflaeche} m²` : ''}
            {obj.baujahr ? ` · Bj. ${obj.baujahr}` : ''}
            {obj.groesse_qm ? ` · Grundstück ${obj.groesse_qm} m²` : ''}
          </div>
          {g.ortstermin_datum && (
            <div style={{ fontSize: 12, color: 'var(--vl-blue)', fontWeight: 600, marginTop: 6 }}>
              Termin: {new Date(g.ortstermin_datum).toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' })}
              {g.ortstermin_uhrzeit ? ` um ${g.ortstermin_uhrzeit} Uhr` : ''}
            </div>
          )}
        </div>

        {/* ── Kontaktpersonen für Zugang ── */}
        {kontakte.length > 0 && (
          <div style={{ marginBottom: 'var(--space-4)' }}>
            <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-tertiary)', marginBottom: 6 }}>
              Kontakt vor Ort
            </div>
            {kontakte.map((k, i) => (
              <div key={i} style={{ fontSize: 13, marginBottom: 4 }}>
                <span style={{ fontWeight: 600 }}>{k.name}</span>
                <span style={{ color: 'var(--text-tertiary)', marginLeft: 6 }}>({k.rolle})</span>
                {k.anschrift && <div style={{ fontSize: 11, color: 'var(--text-secondary)', marginLeft: 0 }}>{k.anschrift}</div>}
              </div>
            ))}
          </div>
        )}

        {/* ── Rundgang-Bereiche (im Aufnahme-Tab ausgeblendet, da Live-Fortschritt dort) ── */}
        {!hideBereiche && (
        <div style={{ marginBottom: 'var(--space-4)' }}>
          <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-tertiary)', marginBottom: 8 }}>
            Rundgang-Bereiche ({bereicheDone}/{bereicheTotal})
          </div>
          {(() => {
            const pill = (b) => (
              <div key={b.id} style={{
                fontSize: 12, padding: '4px 10px', borderRadius: 99,
                background: b.done ? 'var(--success-bg)' : 'var(--surface-light)',
                color: b.done ? 'var(--success)' : 'var(--text-secondary)',
                border: `1px solid ${b.done ? 'var(--success)' : 'var(--border-light)'}`,
                fontWeight: b.done ? 600 : 400,
              }}>
                {b.done ? '✓ ' : '○ '}{b.label}
              </div>
            );
            const hatEigentum = bereicheStatus.some(b => b.eigentum);
            if (!hatEigentum) {
              return (
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                  {bereicheStatus.map(pill)}
                </div>
              );
            }
            const ge = bereicheStatus.filter(b => b.eigentum === 'ge');
            const se = bereicheStatus.filter(b => b.eigentum === 'se');
            const rest = bereicheStatus.filter(b => !b.eigentum);
            const grp = (label, arr) => arr.length === 0 ? null : (
              <div style={{ marginBottom: 6 }}>
                <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-tertiary)', marginBottom: 4 }}>{label}</div>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>{arr.map(pill)}</div>
              </div>
            );
            return (
              <>
                {grp('Gemeinschaftseigentum', ge)}
                {grp('Sondereigentum (Wohnung)', se)}
                {rest.length > 0 && <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>{rest.map(pill)}</div>}
              </>
            );
          })()}
        </div>
        )}

        {/* ── Objekttyp-Hinweise (immer sichtbar, auch wenn Bereiche ausgeblendet) ── */}
        {objektHinweise.length > 0 && (
          <div style={{
            marginBottom: 'var(--space-4)', padding: '8px 12px',
            background: 'rgba(37, 99, 235, 0.04)',
            border: '1px solid rgba(37, 99, 235, 0.12)',
            borderRadius: 'var(--radius-sm)',
          }}>
            <div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em', color: 'var(--vl-blue)', marginBottom: 4 }}>
              Hinweise für {rundgangProfil.profil.label}
            </div>
            <ul style={{ margin: 0, paddingLeft: 16, fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
              {objektHinweise.map((h, i) => <li key={i}>{h}</li>)}
            </ul>
          </div>
        )}

        {/* ── Fehlende Stammdaten ── */}
        {fehlend.length > 0 && (
          <div style={{ marginBottom: 'var(--space-4)' }}>
            <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-tertiary)', marginBottom: 8 }}>
              Vor Ort prüfen ({fehlend.length} offen)
            </div>
            {fehlend.map((f, i) => (
              <div key={i} style={{
                display: 'flex', gap: 8, alignItems: 'flex-start',
                padding: '6px 0', borderBottom: i < fehlend.length - 1 ? '1px solid var(--border-light)' : 'none',
              }}>
                <span style={{
                  fontSize: 10, fontWeight: 700, padding: '1px 6px', borderRadius: 4, flexShrink: 0, marginTop: 2,
                  background: f.prio === 'hoch' ? 'rgba(220,38,38,0.1)' : f.prio === 'mittel' ? 'rgba(234,179,8,0.1)' : 'var(--surface-light)',
                  color: f.prio === 'hoch' ? 'var(--danger)' : f.prio === 'mittel' ? '#B45309' : 'var(--text-tertiary)',
                }}>
                  {f.prio === 'hoch' ? '!' : f.prio === 'mittel' ? '?' : '·'}
                </span>
                <div>
                  <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)' }}>{f.label}</div>
                  <div style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{f.hint}</div>
                </div>
              </div>
            ))}
          </div>
        )}

        {/* ── Fehlende Unterlagen ── */}
        {fehlendeUnterlagen.length > 0 && (
          <div>
            <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-tertiary)', marginBottom: 8 }}>
              Fehlende Unterlagen ({fehlendeUnterlagen.length})
            </div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {fehlendeUnterlagen.map(u => (
                <div key={u.id} style={{
                  fontSize: 12, padding: '4px 10px', borderRadius: 99,
                  background: 'rgba(220,38,38,0.05)', color: 'var(--danger)',
                  border: '1px solid rgba(220,38,38,0.15)',
                }}>
                  {u.label}
                  {u.askOwner && <span style={{ fontSize: 10, marginLeft: 4, opacity: 0.6 }}>← Eigentümer</span>}
                </div>
              ))}
            </div>
          </div>
        )}

        {/* ── Alles gut ── */}
        {fehlend.length === 0 && fehlendeUnterlagen.length === 0 && (
          <div style={{ fontSize: 13, color: 'var(--success)', fontWeight: 500 }}>
            ✓ Alle Stammdaten und Unterlagen vollständig. Bereit für den Rundgang.
          </div>
        )}
      </div>
    </div>
  );
};

// Style für die Einträge im "Notiz importieren"-Menü
const importMenuItemStyle = {
  display: 'flex', alignItems: 'center', gap: 12,
  width: '100%', padding: '12px 14px',
  background: 'transparent', border: 'none', borderBottom: '1px solid var(--border-light)',
  cursor: 'pointer', textAlign: 'left', color: 'var(--text-primary)',
  transition: 'background 0.12s',
};

// ── Foto-Zuordnungs-Overlay (Transparenz/UX) ──
// Zeigt die ausgewählten/abgelegten Fotos als Vorschau-Raster. Oben ein
// "Bereich für alle"-Dropdown (Startwert), darunter jedes Foto mit eigenem
// Dropdown (Ausnahmen). Erst "Übernehmen" speichert. Dient auch der nachträglichen
// Neuzuordnung bereits gespeicherter Fotos (modus='nachtraeglich').
const FotoZuordnungOverlay = ({ items: initialItems, areas, saving, onConfirm, onCancel, isMobile }) => {
  const [items, setItems] = useState(initialItems);

  const setAlle = (kapitel) => {
    setItems(items.map(it => ({ ...it, kapitel: kapitel || null })));
  };
  const setEinzeln = (id, kapitel) => {
    setItems(items.map(it => it.id === id ? { ...it, kapitel: kapitel || null } : it));
  };

  const labelFor = (kid) => (areas.find(a => (a.id || '') === (kid || ''))?.label) || 'Ohne Zuordnung';

  return (
    <div className="modal-backdrop" onClick={e => { if (e.target === e.currentTarget && !saving) onCancel(); }}>
      <div className="modal" style={{ maxWidth: 720, width: '100%', display: 'flex', flexDirection: 'column', maxHeight: '88vh' }}>
        <div className="modal-header">
          <span>{items.length} {items.length === 1 ? 'Foto' : 'Fotos'} zuordnen</span>
          {!saving && <button className="modal-close" onClick={onCancel}>×</button>}
        </div>

        {/* Bereich für alle */}
        <div style={{ padding: '12px 20px', borderBottom: '1px solid var(--border-light)', display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          <span style={{ fontSize: 13, color: 'var(--text-secondary)', fontWeight: 600 }}>Bereich für alle:</span>
          <select
            onChange={e => setAlle(e.target.value || null)}
            defaultValue=""
            disabled={saving}
            style={{ flex: '1 1 200px', minHeight: 38, padding: '0 10px', fontSize: 13,
                     border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
                     background: 'var(--surface)', cursor: 'pointer' }}
          >
            <option value="" disabled>— wählen —</option>
            {areas.map(a => <option key={a.id || '_none'} value={a.id || ''}>{a.label}</option>)}
          </select>
          <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>einzeln unten anpassbar</span>
        </div>

        {/* Foto-Raster */}
        <div style={{ padding: 16, overflowY: 'auto', flex: 1, display: 'grid',
                      gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, 1fr)', gap: 12 }}>
          {items.map(it => (
            <div key={it.id} style={{ border: '1px solid var(--border-light)', borderRadius: 'var(--radius-md)', overflow: 'hidden', background: 'var(--surface-light)' }}>
              <div style={{ width: '100%', height: 140, background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                <img src={it.previewUrl} alt="" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
              </div>
              <div style={{ padding: 8 }}>
                <select
                  value={it.kapitel || ''}
                  onChange={e => setEinzeln(it.id, e.target.value || null)}
                  disabled={saving}
                  style={{ width: '100%', minHeight: 34, padding: '0 8px', fontSize: 12,
                           border: `1px solid ${it.kapitel ? 'var(--success, #0D7A4F)' : 'var(--border-light)'}`,
                           borderRadius: 'var(--radius-sm)', background: 'var(--surface)', cursor: 'pointer',
                           color: it.kapitel ? 'var(--text-primary)' : 'var(--text-tertiary)' }}
                >
                  {areas.map(a => <option key={a.id || '_none'} value={a.id || ''}>{a.label}</option>)}
                </select>
              </div>
            </div>
          ))}
        </div>

        {/* Footer */}
        <div className="modal-footer" style={{ padding: '12px 20px', borderTop: '1px solid var(--border-light)', display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
          <button className="btn btn-ghost" onClick={onCancel} disabled={saving}>Abbrechen</button>
          <button className="btn btn-primary" onClick={() => onConfirm(items)} disabled={saving}>
            {saving ? 'Speichere…' : 'Übernehmen'}
          </button>
        </div>
      </div>
    </div>
  );
};

const OrtsterminTab = ({ p, g, aktiverGutachtenIdx, session, workerUrl, userProfile, onRefresh }) => {
  const isMobile = useIsMobile();
  const showToast = useToast();
  const multiG = p.gutachten.length > 1;
  const objekte = g.objekte || [];
  const objekteForRundgang = objekte.length > 0 ? objekte : [];

  // Objekt-Zuordnung: Bei 1 Objekt automatisch, bei mehreren per Auswahl
  const [selectedObjektId, setSelectedObjektId] = useState(() =>
    objekte.length === 1 ? objekte[0].id : null
  );
  // Auto-Update wenn Objekte sich ändern
  useEffect(() => {
    if (objekte.length === 1) setSelectedObjektId(objekte[0].id);
  }, [objekte.length]);

  const [notes, setNotes] = useState([]);
  const [notesLoading, setNotesLoading] = useState(false);

  // ─── splitResult-Persistenz (überlebt Tab-Wechsel innerhalb der App) ───
  // Problem: Wenn User von Ortstermin-Tab wegnavigiert und zurückkommt, wird
  // OrtsterminTab unmounted/remounted → splitResult-State geht verloren.
  // Lösung: splitResult in sessionStorage zwischenspeichern (pro Gutachten-ID).
  const [mode, setMode] = useState(() => {
    try {
      return sessionStorage.getItem(`splitResult_${g.id}`) ? 'review' : 'idle';
    } catch { return 'idle'; }
  });
  const [splitResult, setSplitResult] = useState(() => {
    try {
      const saved = sessionStorage.getItem(`splitResult_${g.id}`);
      return saved ? JSON.parse(saved) : null;
    } catch { return null; }
  });

  // Sync splitResult → sessionStorage
  useEffect(() => {
    const key = `splitResult_${g.id}`;
    try {
      if (splitResult && mode === 'review') {
        sessionStorage.setItem(key, JSON.stringify(splitResult));
      } else if (!splitResult) {
        sessionStorage.removeItem(key);
      }
    } catch {}
  }, [splitResult, mode, g.id]);

  // ─── Text-Import: Modal-State für Phase-5c-Workflow ───
  const [showTranscriptImport, setShowTranscriptImport] = useState(false);

  // ─── Handschrift-OCR: Fotos sammeln → transkribieren → Split→Review ───
  const [handschriftOpen, setHandschriftOpen] = useState(false);
  const [handschriftFotos, setHandschriftFotos] = useState([]); // [{ id, previewUrl, blob, mediaType }]
  const [handschriftBusy, setHandschriftBusy] = useState(null);  // null | 'ocr' | 'split'
  const handschriftInputRef = useRef(null);

  // ─── Offline-First Layer ───
  const offline = useOrtsterminOffline(g.id);
  const lastDraftRef = useRef('');

  // Kontext für Voice-Hook
  const recentNotesForContext = useMemo(
    () => notes.filter(n => n.kapitel !== 'transcript_raw').slice(-3).map(n => n.text).filter(Boolean),
    [notes]
  );

  const getTranscribeContext = useCallback(() => ({
    profession: 'bewertung',
    objectType: objekte[0]?.bezeichnung || '',
    projectAddress: g.adresse || '',
    recentNotes: recentNotesForContext,
  }), [g.adresse, objekte, recentNotesForContext]);

  // Audio-Chunk-Callback: Jeder 10s-Chunk wird sofort in IDB gesichert
  const handleChunkSaved = useCallback(async (blob, chunkIdx) => {
    try {
      // Überschreibt immer denselben Key = kumulativer Safety-Blob
      await idbPut(STORE_AUDIO, {
        id: `safety_${g.id}`,
        gutachten_id: g.id,
        blob,
        mimeType: blob.type || 'audio/webm',
        sizeBytes: blob.size,
        timestamp: Date.now(),
        durationMs: chunkIdx * 10000,
        tagId: 'recording',
        objektId: null,
        status: 'recording',
        transcription: null,
      });
    } catch (e) { console.warn('[IDB] Chunk save failed:', e); }
  }, [g.id]);

  const voice = useVoice(null, getTranscribeContext, workerUrl, session, handleChunkSaved);
  const rundgang = useRundgang({
    availableTags: VL_KAPITEL_TAGS,
    kapitelTags: VL_KAPITEL_TAGS,
  });

  // Objekte für Rundgang-Detection setzen
  useEffect(() => {
    rundgang.setAvailableObjekte(objekteForRundgang);
  }, [objekteForRundgang, rundgang]);

  // Notes initial laden
  useEffect(() => {
    if (!g.id) return;
    let active = true;
    setNotesLoading(true);
    ladeNotes(g.id)
      .then(rows => { if (active) { setNotes(rows); setNotesLoading(false); } })
      .catch(err => { console.warn(err); if (active) setNotesLoading(false); });
    return () => { active = false; };
  }, [g.id]);

  const refreshNotes = async () => {
    const rows = await ladeNotes(g.id);
    setNotes(rows);
  };

  // ─── Auto-Draft: Alle 15s den aktuellen Text in IDB sichern ───
  const voiceTextRef = useRef('');
  useEffect(() => { voiceTextRef.current = voice.text || ''; }, [voice.text]);

  useEffect(() => {
    if (mode !== 'recording') return;
    const interval = setInterval(async () => {
      const currentText = voiceTextRef.current;
      if (currentText.length > 20 && currentText !== lastDraftRef.current) {
        lastDraftRef.current = currentText;
        try {
          await idbPut(STORE_PENDING, {
            localId: `draft_${g.id}`,
            gutachten_id: g.id,
            noteRow: {
              project_id: p.id,
              gutachten_id: g.id,
              type: 'voice',
              text: currentText,
              kapitel: 'draft',
            },
            syncStatus: 'draft',
            createdAt: Date.now(),
            serverId: null,
          });
        } catch {}
      }
    }, 15000);
    return () => clearInterval(interval);
  }, [mode, g.id, p.id]);

  // ─── Einzel-Notiz starten ───
  // ─── Aufnahme starten (ein Modus, Live-Tagging immer aktiv) ───
  // Die frühere Unterscheidung Einzelnotiz/Rundgang entfällt: das System
  // entscheidet beim Stoppen anhand von Länge + erkannten Kapiteln, ob die
  // Aufnahme direkt als eine Notiz gespeichert oder per KI aufgeteilt wird.
  const [todoDecision, setTodoDecision] = useState(null);  // { text } → Rückfrage: Befund-Notiz vs. Aufgabe
  const pendingTodoRef = useRef(false);  // true → nächste Aufnahme als ToDo speichern
  const startRecording = async (asTodo = false) => {
    pendingTodoRef.current = !!asTodo;
    rundgang.reset();
    voice.reset();
    rundgang.onStart();          // Live-Tagging immer an — hilft auch kurzen Notizen
    setMode('recording');
    await voice.start();
  };

  // ─── Aufnahme stoppen — intelligente Verzweigung ───
  // Entscheidung: kurz UND höchstens ein Kapitel erkannt → direkt als eine
  // Notiz speichern (kein KI-Call). Sonst (lang ODER mehrere Kapitel) → KI-Split
  // mit Review wie der frühere Rundgang.
  const SPLIT_SCHWELLE_ZEICHEN = 300;

  // Diktat als Aufgabe in die Aktivitäten ablegen (Titel = erster Satz, Rest = Inhalt).
  // UI-Reset macht der Aufrufer.
  const persistTodo = async (text) => {
    const satzEnde = text.search(/[.!?]\s/);
    const titel = (satzEnde > 0 && satzEnde < 80)
      ? text.slice(0, satzEnde + 1).trim()
      : (text.length > 80 ? text.slice(0, 80).trim() + '…' : text);
    const inhalt = text.length > titel.length ? text : null;
    const aufgabeRow = {
      project_id: p.id,
      typ: 'aufgabe',
      titel,
      inhalt: (inhalt && inhalt !== titel) ? inhalt : null,
    };
    let gespeichert = false;
    try {
      await apiInsertRow('aktivitaeten', aufgabeRow, session, workerUrl);
      gespeichert = true;
    } catch (e) {
      console.warn('[ToDo] Aktivität-Insert fehlgeschlagen (evtl. offline):', e.message);
    }
    if (!gespeichert) {
      try {
        await offline.saveNoteOffline({
          project_id: p.id, gutachten_id: g.id, kapitel: 'maengel',
          type: 'voice', text: `TODO: ${text}`,
          user_id: userProfile?.id || null, sort_order: notes.length,
        });
        await refreshNotes();
      } catch (e2) { console.error('[ToDo] Auch Offline-Fallback fehlgeschlagen:', e2); }
    }
    if (gespeichert) { try { showToast('ToDo als Aufgabe gespeichert', 'success'); } catch {} }
    else { alert('Offline — das ToDo wurde lokal gesichert und beim nächsten Online-Sync als Notiz hochgeladen.'); }
  };

  // Diktat als (noch nicht zugeordnete) Befund-Notiz speichern — offline-first wie Pfad A.
  // UI-Reset macht der Aufrufer.
  const persistBefundNote = async (text) => {
    const noteRow = {
      project_id: p.id, gutachten_id: g.id, objekt_id: selectedObjektId,
      kapitel: null, type: 'voice', text,
      user_id: userProfile?.id || null, sort_order: notes.length,
    };
    try {
      const localId = `note_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
      try {
        await idbPut(STORE_PENDING, { localId, gutachten_id: g.id, noteRow, syncStatus: 'pending', serverId: null, createdAt: Date.now() });
      } catch (e) { console.warn('[IDB] Save failed:', e.message); }
      try {
        const serverNote = await insertNote(noteRow);
        if (serverNote?.id) {
          try { await idbPut(STORE_PENDING, { localId, gutachten_id: g.id, noteRow, syncStatus: 'synced', serverId: serverNote.id, createdAt: Date.now() }); } catch {}
        }
      } catch (e) { console.warn('[Sync] Direct insert failed, will retry later:', e.message); }
      await refreshNotes();
      try { showToast('Als Befund-Notiz gespeichert', 'success'); } catch {}
    } catch (err) { console.error('[Ortstermin] Save failed completely:', err); }
  };

  const stopRecording = async () => {
    setMode('processing');
    const result = await voice.stop();
    const finalText = result?.text || voice.text || '';
    const trimmed = finalText.trim();

    // Audio-Chunk immer sichern (offline-safe)
    if (result?.audioBlob && result.audioBlob.size > 1024) {
      try {
        await offline.saveAudioChunk(result.audioBlob, {
          durationMs: result.durationMs || 0,
          tagId: rundgang.currentTagId || 'rundgang',
          objektId: selectedObjektId,
        });
      } catch (e) { console.warn('[IDB] Audio save failed:', e); }
      try { await idbDelete(STORE_AUDIO, `safety_${g.id}`); } catch {}
    }

    // Leere Aufnahme → nichts tun
    if (trimmed.length === 0) {
      setMode('idle');
      voice.reset();
      lastDraftRef.current = '';
      try { await idbDelete(STORE_PENDING, `draft_${g.id}`); } catch {}
      return;
    }

    // Erkannte Kapitel sammeln (Live-Segmente + aktuelles Kapitel)
    const erkannteKapitel = new Set((rundgang.segments || []).map(s => s.tagId).filter(Boolean));
    if (rundgang.currentTagId) erkannteKapitel.add(rundgang.currentTagId);
    const istKurz = trimmed.length < SPLIT_SCHWELLE_ZEICHEN;
    const einKapitel = erkannteKapitel.size <= 1;
    const istTodo = pendingTodoRef.current;
    pendingTodoRef.current = false;  // Flag zurücksetzen

    // ── ToDo-Pfad: als Aufgabe in die Aktivitäten (NICHT als Mängel-Notiz) ──
    //    Ein gesprochenes ToDo ist Arbeitsorganisation, kein Gutachten-Befund.
    //    Wirkt das Diktat aber wie ein Befund (mehrsätzig oder lang), fragen wir
    //    nach, statt es still als Aufgabe abzulegen — verhindert, dass Befunde
    //    versehentlich (z.B. ToDo-Button statt Aufnahme) in den Aktivitäten landen.
    if (istTodo) {
      const satzAnzahl = (trimmed.match(/[.!?](\s|$)/g) || []).length;
      const wirktWieBefund = satzAnzahl >= 2 || trimmed.length > 140;
      // Aufnahme ist beendet → UI in jedem Fall zurücksetzen
      setMode('idle');
      voice.reset();
      lastDraftRef.current = '';
      try { await idbDelete(STORE_PENDING, `draft_${g.id}`); } catch {}
      if (wirktWieBefund) {
        setTodoDecision({ text: trimmed });  // Rückfrage: Befund-Notiz vs. Aufgabe
        return;
      }
      await persistTodo(trimmed);
      return;
    }

    // ── Pfad A: kurz & ein Kapitel → direkt als eine Notiz speichern ──
    if (istKurz && einKapitel) {
      const kapitel = rundgang.currentTagId || (erkannteKapitel.size === 1 ? [...erkannteKapitel][0] : null);
      const noteText = trimmed;
      const noteRow = {
        project_id: p.id,
        gutachten_id: g.id,
        objekt_id: selectedObjektId,
        kapitel: kapitel || null,
        type: 'voice',
        text: noteText,
        user_id: userProfile?.id || null,
        sort_order: notes.length,
      };
      try {
        const localId = `note_${Date.now()}_${Math.random().toString(36).slice(2,6)}`;
        try {
          await idbPut(STORE_PENDING, {
            localId, gutachten_id: g.id, noteRow,
            syncStatus: 'pending', serverId: null, createdAt: Date.now(),
          });
        } catch (e) { console.warn('[IDB] Save failed:', e.message); }
        try {
          const serverNote = await insertNote(noteRow);
          if (serverNote?.id) {
            try {
              await idbPut(STORE_PENDING, {
                localId, gutachten_id: g.id, noteRow,
                syncStatus: 'synced', serverId: serverNote.id, createdAt: Date.now(),
              });
            } catch {}
          }
        } catch (e) { console.warn('[Sync] Direct insert failed, will retry later:', e.message); }
        await refreshNotes();
      } catch (err) {
        console.error('[Ortstermin] Save failed completely:', err);
      }
      setMode('idle');
      voice.reset();
      lastDraftRef.current = '';
      try { await idbDelete(STORE_PENDING, `draft_${g.id}`); } catch {}
      return;
    }

    // ── Pfad B: lang oder mehrere Kapitel → KI-Split + Review ──
    try {
      const res = await splitRundgangMitKI(
        finalText,
        rundgang.segments,
        rundgang.markers,
        {
          objectType: objekte[0]?.bezeichnung || '',
          address: g.adresse || '',
          objekte: objekte.map(o => ({ id: o.id, bezeichnung: o.bezeichnung })),
        },
        session, workerUrl
      );
      setSplitResult(res);
      setMode('review');
    } catch (err) {
      console.error('[Rundgang] Split-Fehler:', err);
      // Offline-Fallback: Text als einzelne Notiz in IDB sichern
      await offline.saveNoteOffline({
        project_id: p.id,
        gutachten_id: g.id,
        kapitel: rundgang.currentTagId || 'allgemein',
        type: 'voice',
        text: trimmed,
        user_id: userProfile?.id || null,
        sort_order: notes.length,
      });
      await refreshNotes();
      alert('KI-Zuordnung fehlgeschlagen. Der Aufnahme-Text wurde als Notiz gesichert und kann manuell zugeordnet werden.');
      setMode('idle');
      voice.reset();
    }
  };

  // ─── Rundgang-Ergebnis akzeptieren (offline-first) ───
  const acceptRundgangResult = async (segments) => {
    try {
      const allRows = [];
      let sortCursor = notes.length;

      // 0. Rohtranskript als eigene, klar gekennzeichnete Notiz zuerst speichern
      //    (nur beim Text-Import vorhanden — bei Live-Aufnahme gibt es kein
      //    separates Rohtranskript). Dient als wortgetreuer Beleg; die KI-
      //    Segmentierung darf nichts verschlucken, im Zweifel hier nachschlagbar.
      if (splitResult.rawTranscript && splitResult.rawTranscript.trim()) {
        allRows.push({
          project_id: p.id,
          gutachten_id: g.id,
          objekt_id: selectedObjektId,
          kapitel: 'transcript_raw',
          // type='text' statt eines neuen Enums — vermeidet Risiko durch einen
          // möglichen CHECK-Constraint auf notes.type. Erkennung erfolgt über
          // kapitel='transcript_raw'.
          type: 'text',
          text: splitResult.rawTranscript.trim(),
          user_id: userProfile?.id || null,
          sort_order: sortCursor++,
        });
      }

      // 1. Geclusterte Segmente
      const segmentRows = segments.map((s) => ({
        project_id: p.id,
        gutachten_id: g.id,
        objekt_id: s.objektId || selectedObjektId,
        kapitel: s.tagId || null,
        type: 'voice',
        text: s.text,
        user_id: userProfile?.id || null,
        sort_order: sortCursor++,
      }));
      allRows.push(...segmentRows);

      // 1. IndexedDB zuerst (offline-safe, niemals verlieren)
      const localIds = await offline.saveNotesOfflineBulk(allRows);

      // 2. Supabase versuchen (best-effort)
      try {
        await insertNotesBulk(allRows);
        // IDB-Items als synced markieren → verhindert Duplikat durch Auto-Sync
        for (const lid of localIds) {
          try { await idbPut(STORE_PENDING, { localId: lid, gutachten_id: g.id, noteRow: {}, syncStatus: 'synced', createdAt: Date.now(), serverId: 'bulk' }); } catch {}
        }
      } catch (e) { console.warn('[Sync] Bulk insert failed, will retry:', e.message); }

      // Todos optional als eigene Notes speichern
      if (splitResult.todos && splitResult.todos.length > 0) {
        const todoRows = splitResult.todos.map((t) => ({
          project_id: p.id,
          gutachten_id: g.id,
          kapitel: 'maengel',
          type: 'text',
          text: 'TODO: ' + t.text,
          user_id: userProfile?.id || null,
          sort_order: sortCursor++,
        }));
        const todoLocalIds = await offline.saveNotesOfflineBulk(todoRows);
        try {
          await insertNotesBulk(todoRows);
          for (const lid of todoLocalIds) {
            try { await idbPut(STORE_PENDING, { localId: lid, gutachten_id: g.id, noteRow: {}, syncStatus: 'synced', createdAt: Date.now(), serverId: 'bulk' }); } catch {}
          }
        } catch {}
      }

      await refreshNotes();
      setSplitResult(null);
      setMode('idle');
      voice.reset();
      rundgang.reset();
    } catch (err) {
      alert('Fehler beim Speichern: ' + (err.message || err));
    }
  };

  const discardRundgangResult = () => {
    if (!confirm('Alle Rundgang-Segmente verwerfen?')) return;
    setSplitResult(null);
    setMode('idle');
    voice.reset();
    rundgang.reset();
  };

  // ─── Foto-Aufnahme mit Bereich-Zuordnung ───
  const fileInputRef = useRef(null);
  const galleryInputRef = useRef(null); // separater Input für Mehrfach-Auswahl (Galerie)
  const [isDraggingPhoto, setIsDraggingPhoto] = useState(false); // Drag&Drop-Hervorhebung (Desktop)
  // Foto-Zuordnungs-Overlay: nach Auswahl/Drop werden Fotos hier gesammelt und
  // erst nach der Bereich-Zuordnung gespeichert. items: [{ id, blob, previewUrl, kapitel }]
  const [zuordnungOverlay, setZuordnungOverlay] = useState(null); // { items, modus } | null
  const [zuordnungSaving, setZuordnungSaving] = useState(false);
  const [photoKapitel, setPhotoKapitel] = useState(null);
  // "Notiz importieren"-Menü (bündelt Audio-Upload, Text-Import, Handschrift)
  const [importMenuOpen, setImportMenuOpen] = useState(false);
  const importMenuRef = useRef(null);
  useEffect(() => {
    if (!importMenuOpen) return;
    const onClickOutside = (e) => {
      if (importMenuRef.current && !importMenuRef.current.contains(e.target)) {
        setImportMenuOpen(false);
      }
    };
    document.addEventListener('click', onClickOutside);
    return () => document.removeEventListener('click', onClickOutside);
  }, [importMenuOpen]);
  // Dynamische Bereiche basierend auf dem gewählten Objekt
  const selectedObjekt = objekte.find(o => o.id === selectedObjektId);

  const PHOTO_AREAS = useMemo(() => {
    // Gleiche Profil-Funktion wie bereicheStatus — konsistente Bereiche
    const { pflicht, optional } = getRundgangBereiche(selectedObjekt);
    const areas = [{ id: null, label: 'Ohne Zuordnung' }];
    for (const b of [...pflicht, ...optional]) {
      areas.push({ id: b.id, label: b.label });
    }
    return areas;
  }, [selectedObjekt]);

  // Schritt 1: Fotos auswählen/ablegen → komprimieren + Vorschau → Zuordnungs-Overlay
  // (noch NICHT speichern). Der voreingestellte Bereich (photoKapitel) wird als
  // Startwert für alle übernommen, einzeln im Overlay überschreibbar.
  const processPhotoFiles = async (fileList) => {
    const files = Array.from(fileList || []).filter(f => f && f.type?.startsWith('image/'));
    if (files.length === 0) return;
    const items = [];
    for (let i = 0; i < files.length; i++) {
      try {
        const blob = await compressImage(files[i], 1600, 0.82);
        items.push({
          id: `f${Date.now()}_${i}`,
          blob,
          previewUrl: URL.createObjectURL(blob),
          kapitel: photoKapitel || null,
        });
      } catch (err) {
        console.error('Foto-Komprimierung fehlgeschlagen:', err);
      }
    }
    if (items.length > 0) setZuordnungOverlay({ items, modus: 'upload' });
  };

  // Schritt 2: "Übernehmen" im Overlay → alle Fotos mit ihrem jeweiligen Bereich
  // hochladen und als Notiz speichern.
  const speichereZuordnung = async (items) => {
    setZuordnungSaving(true);
    let ok = 0;
    const fehler = [];
    for (let i = 0; i < items.length; i++) {
      const it = items[i];
      try {
        const upResult = await uploadPhotoBlob(it.blob, p.id, g.id, session, workerUrl);
        await insertNote({
          project_id: p.id,
          gutachten_id: g.id,
          objekt_id: selectedObjektId,
          kapitel: it.kapitel,
          type: 'photo',
          image_url: upResult.storage_path,
          user_id: userProfile?.id || null,
          sort_order: notes.length + i,
        });
        ok++;
      } catch (err) {
        fehler.push(`Foto ${i + 1}: ${err.message || err}`);
      }
    }
    // Vorschau-URLs freigeben
    items.forEach(it => { try { URL.revokeObjectURL(it.previewUrl); } catch {} });
    setZuordnungSaving(false);
    setZuordnungOverlay(null);
    await refreshNotes();
    if (fehler.length > 0) {
      alert(`${ok} von ${items.length} Fotos gespeichert.\n\nFehlgeschlagen:\n${fehler.join('\n')}`);
    }
  };

  const handlePhotoCapture = async (e) => {
    // Dateien als echtes Array sichern, BEVOR der Input zurückgesetzt wird —
    // e.target.files ist eine Live-FileList, die durch e.target.value='' geleert würde.
    const files = Array.from(e.target.files || []);
    e.target.value = '';
    await processPhotoFiles(files);
  };

  // Nachträgliche Zuordnung: bereits gespeicherte Fotos im selben Overlay neu zuordnen.
  const oeffneNachtraeglicheZuordnung = async () => {
    const fotoNotes = notes.filter(n => n.type === 'photo' && n.image_url);
    if (fotoNotes.length === 0) return;
    // Signierte URLs für die Vorschau holen
    const sb = await initSupabase();
    const items = [];
    for (const n of fotoNotes) {
      let url = '';
      try {
        const { data } = await sb.storage.from('ortstermin-photos').createSignedUrl(n.image_url, 600);
        url = data?.signedUrl || '';
      } catch {}
      items.push({ id: n.id, noteId: n.id, previewUrl: url, kapitel: n.kapitel || null });
    }
    setZuordnungOverlay({ items, modus: 'nachtraeglich' });
  };

  // Speichert die nachträglichen Zuordnungen (nur geänderte Kapitel patchen).
  const speichereNachtraeglicheZuordnung = async (items) => {
    setZuordnungSaving(true);
    let fehler = 0;
    for (const it of items) {
      try {
        await updateNote(it.noteId, { kapitel: it.kapitel });
      } catch (err) {
        fehler++;
      }
    }
    setZuordnungSaving(false);
    setZuordnungOverlay(null);
    await refreshNotes();
    if (fehler > 0) alert(`${fehler} Zuordnung(en) konnten nicht gespeichert werden.`);
  };

  // ─── Phase 5c: Text-Import (Transkript einfügen statt Audio hochladen) ───
  // Bei Erfolg returnt true → Modal wird vom Parent geschlossen.
  // Bei Fehler bleibt das Modal offen, damit der User den eingegebenen
  // Text nicht verliert und nochmal versuchen kann.
  const handleTranscriptImport = async (transcriptText) => {
    const trimmed = (transcriptText || '').trim();
    if (trimmed.length < 50) {
      alert('Transkript zu kurz. Bitte mindestens 50 Zeichen einfügen.');
      return false;
    }
    if (trimmed.length > 60000) {
      alert('Transkript zu lang (max. 60.000 Zeichen). Bitte in Abschnitten importieren.');
      return false;
    }
    setMode('processing');
    try {
      const res = await splitRundgangMitKI(
        trimmed,
        [],  // keine live_segments — Text kommt aus externer Quelle
        [],  // keine markers
        {
          objectType: objekte[0]?.bezeichnung || '',
          address: g.adresse || '',
          objekte: objekte.map(o => ({ id: o.id, bezeichnung: o.bezeichnung })),
        },
        session, workerUrl
      );
      if (!res.ok) {
        const detail = res.error ? `\n\nFehler: ${res.error}` : '';
        alert(`KI-Zuordnung fehlgeschlagen.${detail}\n\nBitte prüfe die Browser-DevTools (Console & Network → /api/split-rundgang) für mehr Details, oder versuche es erneut.`);
        setMode('idle');
        return false;
      }
      setShowTranscriptImport(false);
      setSplitResult({ ...res, rawTranscript: trimmed });
      setMode('review');
      return true;
    } catch (err) {
      console.error('[TranscriptImport] Split fehlgeschlagen:', err);
      alert('Fehler beim Verarbeiten des Transkripts: ' + (err.message || err));
      setMode('idle');
      return false;
    }
  };

  // ─── Handschrift-OCR: Fotos abfotografieren → Claude transkribiert → Split→Review ───
  const blobToBase64 = (blob) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const dataUrl = reader.result;
      const comma = dataUrl.indexOf(',');
      resolve(dataUrl.slice(comma + 1)); // base64 ohne data:-Präfix
    };
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });

  const handleHandschriftAdd = async (e) => {
    const files = Array.from(e.target.files || []);
    e.target.value = '';
    if (files.length === 0) return;
    for (const file of files) {
      if (handschriftFotos.length >= 10) {
        alert('Maximal 10 Seiten auf einmal.');
        break;
      }
      try {
        const blob = await compressImage(file, 2000, 0.85); // höhere Auflösung für Lesbarkeit der Handschrift
        const previewUrl = URL.createObjectURL(blob);
        setHandschriftFotos(prev => [...prev, {
          id: `hs_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
          previewUrl, blob, mediaType: blob.type || 'image/jpeg',
        }]);
      } catch (err) {
        console.warn('[Handschrift] Bild-Komprimierung fehlgeschlagen:', err);
      }
    }
  };

  const handleHandschriftRemove = (id) => {
    setHandschriftFotos(prev => {
      const item = prev.find(f => f.id === id);
      if (item?.previewUrl) { try { URL.revokeObjectURL(item.previewUrl); } catch {} }
      return prev.filter(f => f.id !== id);
    });
  };

  const handleHandschriftTranscribe = async () => {
    if (handschriftFotos.length === 0 || handschriftBusy) return;
    setHandschriftBusy('ocr');
    try {
      // 1. Belege hochladen (jedes Foto als type='photo'-Notiz)
      for (const foto of handschriftFotos) {
        try {
          const upResult = await uploadPhotoBlob(foto.blob, p.id, g.id, session, workerUrl);
          await insertNote({
            project_id: p.id,
            gutachten_id: g.id,
            objekt_id: selectedObjektId,
            kapitel: null,
            type: 'photo',
            image_url: upResult.storage_path,
            user_id: userProfile?.id || null,
            sort_order: notes.length,
          });
        } catch (upErr) {
          console.warn('[Handschrift] Beleg-Upload fehlgeschlagen (nicht fatal):', upErr.message);
        }
      }

      // 2. Bilder → base64 → OCR-Endpoint
      const images = await Promise.all(handschriftFotos.map(async (foto) => ({
        media_type: foto.mediaType,
        data: await blobToBase64(foto.blob),
      })));

      const ocrRes = await fetch(`${workerUrl}/api/ocr-handschrift`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${session.access_token}` },
        body: JSON.stringify({ images }),
      });
      if (!ocrRes.ok) {
        let msg = 'Transkription fehlgeschlagen';
        try { const e = await ocrRes.json(); if (e.error) msg = e.error; } catch {}
        throw new Error(msg);
      }
      const ocrData = await ocrRes.json();
      const erkannterText = (ocrData.text || '').trim();
      if (!erkannterText) throw new Error('Keine Handschrift erkannt.');

      // 3. Belege aufräumen, Modal schließen, Text durch Split→Review
      handschriftFotos.forEach(f => { if (f.previewUrl) try { URL.revokeObjectURL(f.previewUrl); } catch {} });
      setHandschriftFotos([]);
      setHandschriftOpen(false);
      await refreshNotes();  // Beleg-Fotos sichtbar machen

      setHandschriftBusy('split');
      setMode('processing');
      const res = await splitRundgangMitKI(
        erkannterText, [], [],
        {
          objectType: objekte[0]?.bezeichnung || '',
          address: g.adresse || '',
          objekte: objekte.map(o => ({ id: o.id, bezeichnung: o.bezeichnung })),
        },
        session, workerUrl
      );
      if (!res.ok) {
        await offline.saveNoteOffline({
          project_id: p.id, gutachten_id: g.id, kapitel: 'allgemein',
          type: 'text', text: erkannterText, user_id: userProfile?.id || null, sort_order: notes.length,
        });
        await refreshNotes();
        setMode('idle');
        alert('Handschrift wurde transkribiert und als Notiz gesichert, aber die automatische Sortierung schlug fehl. Du kannst die Notiz manuell zuordnen.');
        return;
      }
      setSplitResult({ ...res, rawTranscript: erkannterText });
      setMode('review');
    } catch (err) {
      console.error('[Handschrift] Fehler:', err);
      alert('Fehler bei der Handschrift-Erkennung: ' + (err.message || err));
      setMode('idle');
    } finally {
      setHandschriftBusy(null);
    }
  };

  // ─── Audio-Datei Upload (große Dateien, externes Aufnahmegerät) ───
  const audioInputRef = useRef(null);
  const [audioUploadState, setAudioUploadState] = useState(null); // null = idle, sonst { phase, chunk, totalChunks, message, text }

  // ─── Tab-Visibility: Notes + Upload-State aktualisieren wenn User zurückkommt ───
  useEffect(() => {
    const handleVisible = () => {
      if (document.visibilityState === 'visible' && g.id) {
        refreshNotes();
        // Stale Upload-State aufräumen (Transkription im Hintergrund fertig)
        setAudioUploadState(prev => {
          if (prev && (prev.phase === 'background' || prev.phase === 'done')) return null;
          return prev;
        });
      }
    };
    document.addEventListener('visibilitychange', handleVisible);
    return () => document.removeEventListener('visibilitychange', handleVisible);
  }, [g.id]);

  const handleAudioUpload = async (e) => {
    const file = e.target.files?.[0];
    e.target.value = '';
    if (!file) return;

    console.log('[Audio Upload]', file.name, (file.size / 1024 / 1024).toFixed(1), 'MB');

    // Aktuelle Notizen-Anzahl merken um neue Note zu erkennen
    const noteCountBefore = notes.length;
    setAudioUploadState({ phase: 'uploading', chunk: 0, totalChunks: 0, message: 'Datei wird hochgeladen...', text: '' });

    try {
      const formData = new FormData();
      formData.append('audio', file, file.name);
      formData.append('context', JSON.stringify({
        projectAddress: g.adresse || g.address || '',
        objectType: 'EFH',
      }));
      formData.append('project_id', p.id);
      formData.append('gutachten_id', g.id);
      formData.append('user_id', userProfile?.id || '');
      if (selectedObjektId) formData.append('objekt_id', selectedObjektId);

      const res = await fetch(`${workerUrl}/api/transcribe-upload`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${session.access_token}` },
        body: formData,
      });

      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error(err.message || err.error || `HTTP ${res.status}`);
      }

      // SSE-Stream lesen — kann bei Tab-Wechsel abbrechen, das ist OK
      let streamCompleted = false;
      try {
        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            if (!line.startsWith('data: ')) continue;
            let event;
            try { event = JSON.parse(line.substring(6)); } catch { continue; }

            if (event.type === 'start') {
              setAudioUploadState(prev => ({ ...prev, phase: 'transcribing', totalChunks: event.totalChunks, message: `${event.totalSizeMB} MB — ${event.totalChunks} Teil(e)` }));
            } else if (event.type === 'progress') {
              setAudioUploadState(prev => ({ ...prev, phase: event.phase, chunk: event.chunk, message: event.message }));
            } else if (event.type === 'chunk_done') {
              setAudioUploadState(prev => ({ ...prev, chunk: event.chunk, text: event.accumulated || prev.text }));
            } else if (event.type === 'chunk_error') {
              setAudioUploadState(prev => ({ ...prev, message: `Teil ${event.chunk}: ${event.error}` }));
            } else if (event.type === 'complete') {
              streamCompleted = true;
              setAudioUploadState(prev => ({ ...prev, phase: 'done', message: event.saved ? 'Notiz gespeichert.' : 'Fertig.' }));
            } else if (event.type === 'error') {
              throw new Error(event.message);
            }
          }
        }
      } catch (streamErr) {
        // Stream abgebrochen (Tab-Wechsel, Netzwerk) — Worker läuft trotzdem weiter
        console.warn('[Audio Upload] Stream unterbrochen:', streamErr.message, '— Worker verarbeitet im Hintergrund weiter.');
      }

      if (streamCompleted) {
        // Stream lief bis zum Ende durch — Note ist gespeichert, einfach neu laden
        await refreshNotes();
        setAudioUploadState(null);
      } else {
        // Stream wurde unterbrochen — auf Polling umschalten
        setAudioUploadState({ phase: 'background', chunk: 0, totalChunks: 0, message: 'Transkription läuft im Hintergrund. Bitte warten...', text: '' });
        // Alle 4 Sekunden prüfen ob eine neue Note da ist
        const pollForNote = async () => {
          for (let attempt = 0; attempt < 30; attempt++) { // max 2 Minuten
            await new Promise(r => setTimeout(r, 4000));
            const freshNotes = await ladeNotes(g.id);
            if (freshNotes.length > noteCountBefore) {
              console.log('[Audio Upload] Neue Note erkannt nach', attempt + 1, 'Polls');
              setNotes(freshNotes);
              setAudioUploadState(null);
              return;
            }
            setAudioUploadState(prev => prev ? { ...prev, message: `Transkription läuft im Hintergrund... (${(attempt + 1) * 4}s)` } : null);
          }
          // Timeout — trotzdem aufräumen
          console.warn('[Audio Upload] Polling-Timeout nach 2 Minuten');
          await refreshNotes();
          setAudioUploadState(null);
        };
        pollForNote(); // nicht awaiten — läuft im Hintergrund
      }
    } catch (err) {
      console.error('[Audio Upload] Fehler:', err);
      alert('Audio-Transkription fehlgeschlagen: ' + (err.message || err));
      setAudioUploadState(null);
    }
  };

  // ─── Edit/Delete handlers ───
  const handleEditNote = async (id, patch) => {
    await updateNote(id, patch);
    await refreshNotes();
  };
  const handleDeleteNote = async (id) => {
    await deleteNote(id);
    await refreshNotes();
  };

  // ─── Notes nach Kapitel gruppieren ───
  const notesByKapitel = useMemo(() => {
    const knownIds = new Set(VL_KAPITEL_TAGS.map(k => k.id));
    const groups = {};
    for (const n of notes) {
      // Fotos werden im separaten Fotos-Tab angezeigt, nicht hier
      if (n.type === 'photo') continue;
      // Rohtranskripte separat gruppieren (eigener Block, nicht unter "Unkategorisiert")
      if (n.kapitel === 'transcript_raw') {
        if (!groups._transcript_raw) groups._transcript_raw = [];
        groups._transcript_raw.push(n);
        continue;
      }
      // Unbekannte Kapitel-Werte als _none behandeln (damit sie unter "Unkategorisiert" erscheinen)
      const key = (n.kapitel && knownIds.has(n.kapitel)) ? n.kapitel : '_none';
      if (!groups[key]) groups[key] = [];
      groups[key].push(n);
    }
    return groups;
  }, [notes]);

  const kapitelList = VL_KAPITEL_TAGS.filter(k => (notesByKapitel[k.id] || []).length > 0);
  const hasUnkategorisiert = (notesByKapitel['_none'] || []).length > 0;

  const disabled = mode !== 'idle';
  const recording = mode === 'recording';

  // Sub-Tab State
  const [subTab, setSubTab] = useState('aufnahme'); // 'aufnahme' | 'briefing' | 'notizen'
  const [bereicheExpanded, setBereicheExpanded] = useState(false); // Bereiche-Chips eingeklappt (Politur)

  // Bereiche-Status für kompakte Anzeige
  const bereicheStatus = useMemo(() => {
    // Zentrale Profil-Funktion — eine Wahrheit für alle Bereichsableitungen
    const { pflicht } = getRundgangBereiche(selectedObjekt);
    // Notes filtern: nur Notes des gewählten Objekts (+ allgemeine ohne objekt_id)
    const relevantNotes = selectedObjektId
      ? notes.filter(n => n.objekt_id === selectedObjektId || !n.objekt_id)
      : notes;
    return pflicht.map(b => {
      const count = relevantNotes.filter(n => n.kapitel === b.id && n.type !== 'photo').length;
      return {
        id: b.id,
        label: b.label,
        eigentum: b.eigentum,
        done: relevantNotes.some(n => n.kapitel === b.id),
        count,
      };
    });
  }, [selectedObjekt, notes, selectedObjektId]);
  const bereicheDone = bereicheStatus.filter(b => b.done).length;

  // Begehungs-Fortschritt (Cockpit): Bereiche / Notizen / Fotos
  const bereicheTotal = bereicheStatus.length;
  const begehungPct = bereicheTotal > 0 ? bereicheDone / bereicheTotal : 0;
  const begehungColor = begehungPct >= 1 ? 'var(--success)' : begehungPct > 0 ? 'var(--warning)' : 'var(--text-tertiary)';
  const begehungC = 2 * Math.PI * 15;
  const notizenCount = notes.filter(n => n.type !== 'photo' && n.kapitel !== 'transcript_raw').length;
  const fotosCount = notes.filter(n => n.type === 'photo').length;

  // Bereich-Chip → Notizen-Sub-Tab, zum Notiz-Block dieses Bereichs scrollen
  const springeZuBereichNotizen = (kapId) => {
    setSubTab('notizen');
    setTimeout(() => {
      const el = document.getElementById('notiz-kap-' + kapId);
      if (el) {
        el.scrollIntoView({ behavior: 'smooth', block: 'start' });
        el.classList.remove('feld-pulse'); void el.offsetWidth; el.classList.add('feld-pulse');
        setTimeout(() => el.classList.remove('feld-pulse'), 1500);
      }
    }, 80);
  };

  return (
    <>
      {/* ── Review-Modus: Rundgang-Ergebnis prüfen ── */}
      {mode === 'review' && splitResult ? (
        <div className="card">
          <RundgangResult
            splitResult={splitResult}
            objekte={objekte}
            onAccept={acceptRundgangResult}
            onDiscard={discardRundgangResult}
          />
        </div>

      /* ── Aufnahme-Modus: Recording-UI dominiert ── */
      ) : recording || mode === 'processing' ? (
        <div className="card" style={{ minHeight: isMobile ? 'calc(100vh - 140px)' : 400, display: 'flex', flexDirection: 'column' }}>
          <div className="card-header" style={{ flexWrap: 'wrap', gap: 8, flexShrink: 0 }}>
            <span className="card-title" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <span style={{
                width: 12, height: 12, borderRadius: '50%',
                background: recording ? '#DC2626' : 'var(--text-tertiary)',
                animation: recording ? 'pulse 1.5s infinite' : 'none',
              }} />
              <span style={{ fontSize: isMobile ? 16 : 14 }}>
                {mode === 'processing' ? 'Verarbeitung' : 'Aufnahme'}
              </span>
              {voice.elapsedMs > 0 && (
                <span style={{ fontSize: isMobile ? 16 : 13, fontWeight: 400, color: 'var(--text-tertiary)', fontVariantNumeric: 'tabular-nums' }}>
                  {Math.floor(voice.elapsedMs / 60000)}:{String(Math.floor((voice.elapsedMs % 60000) / 1000)).padStart(2, '0')}
                </span>
              )}
            </span>
            <span style={{
              fontSize: 10, fontWeight: 600, padding: '2px 6px', borderRadius: 99,
              background: offline.isOnline ? 'var(--success-bg)' : 'rgba(234,179,8,0.1)',
              color: offline.isOnline ? 'var(--success)' : '#B45309',
            }}>
              {offline.isOnline ? '● Online' : '● Offline'}
              {voice.chunkCount > 0 && ` · ${voice.chunkCount}× gesichert`}
            </span>
          </div>

          {/* Live-Text: nimmt den ganzen verfügbaren Platz */}
          <div style={{
            padding: isMobile ? 'var(--space-3)' : 'var(--space-4)',
            flex: 1, overflowY: 'auto',
          }}>
            {mode === 'processing' ? (
              <div style={{ textAlign: 'center', padding: 'var(--space-5)', color: 'var(--text-secondary)' }}>
                <div style={{ width: 24, height: 24, borderRadius: 12, border: '2.5px solid var(--border)', borderTopColor: 'var(--vl-blue)', animation: 'spin 0.8s linear infinite', margin: '0 auto 12px' }} />
                <div style={{ fontSize: 14 }}>{voice.isTranscribing ? 'Transkription läuft…' : 'Verarbeitung…'}</div>
              </div>
            ) : (
              <div style={{
                fontSize: isMobile ? 17 : 15, lineHeight: 1.7,
                color: 'var(--text-primary)', whiteSpace: 'pre-wrap',
              }}>
                {voice.text || <span style={{ color: 'var(--text-tertiary)', fontStyle: 'italic' }}>
                  {voice.noLivePreview
                    ? 'Aufnahme läuft — Live-Vorschau nicht verfügbar (Safari). Text wird nach dem Stoppen transkribiert.'
                    : 'Sprich in das Mikrofon…'}
                </span>}
                {voice.interim && <span style={{ color: 'var(--text-tertiary)' }}> {voice.interim}</span>}
              </div>
            )}
          </div>

          {/* Aktiver Bereich (Rundgang) */}
          {mode === 'recording' && rundgang.currentTagId && (
            <div style={{
              padding: isMobile ? '10px var(--space-3)' : '8px var(--space-4)',
              borderTop: '1px solid var(--border-light)',
              fontSize: isMobile ? 14 : 12, color: 'var(--vl-blue)', fontWeight: 600,
              flexShrink: 0,
            }}>
              📍 {VL_KAPITEL_TAGS.find(t => t.id === rundgang.currentTagId)?.label || rundgang.currentTagId}
            </div>
          )}

          {/* Stopp-Button: auf Mobile fixed am unteren Rand */}
          {recording && (
            <div style={{
              padding: isMobile ? '12px 16px' : 'var(--space-3) var(--space-4)',
              borderTop: '1px solid var(--border-light)',
              display: 'flex', justifyContent: 'center',
              flexShrink: 0,
              ...(isMobile ? {
                position: 'fixed', bottom: 0, left: 0, right: 0,
                background: 'var(--surface)', zIndex: 50,
                boxShadow: '0 -2px 12px rgba(0,0,0,0.1)',
                paddingBottom: 'max(12px, env(safe-area-inset-bottom))',
              } : {}),
            }}>
              <button
                onClick={stopRecording}
                style={{
                  background: '#DC2626', color: 'white', border: 'none',
                  padding: isMobile ? '16px 40px' : '12px 32px',
                  fontSize: isMobile ? 17 : 15, fontWeight: 600, borderRadius: 99,
                  width: isMobile ? '100%' : undefined,
                  maxWidth: 400,
                  minHeight: 52,
                }}
              >
                ■ Aufnahme stoppen
              </button>
            </div>
          )}

          {/* Unterbrechungs-Banner */}
          {voice.wasInterrupted && (
            <div style={{
              padding: 'var(--space-3)', margin: 'var(--space-3)',
              background: 'rgba(234,179,8,0.1)', color: '#92400E',
              borderRadius: 'var(--radius-sm)', fontSize: 13, border: '1px solid rgba(234,179,8,0.3)',
              flexShrink: 0,
            }}>
              <strong>Aufnahme unterbrochen</strong> — Audio ist gesichert.
              <div style={{ marginTop: 8, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                <button className="btn btn-primary btn-sm" onClick={() => { voice.resume(); setMode(mode === 'idle' ? 'recording' : mode); }}
                  style={{ fontSize: 13, minHeight: 40, padding: '8px 16px' }}>
                  Fortsetzen
                </button>
                <button className="btn btn-ghost btn-sm" onClick={() => { voice.reset(); setMode('idle'); }}
                  style={{ fontSize: 13, minHeight: 40, padding: '8px 16px' }}>
                  Beenden
                </button>
              </div>
            </div>
          )}

          {/* Mic-Error */}
          {voice.micError && (
            <div style={{ padding: 'var(--space-3)', background: 'var(--danger-bg)', color: 'var(--danger)', borderRadius: 'var(--radius-sm)', fontSize: 13, margin: 'var(--space-3)', flexShrink: 0 }}>
              {voice.micError === 'permission' ? 'Mikrofon-Zugriff verweigert. Bitte in den Einstellungen erlauben.'
                : voice.micError === 'browser' ? 'Browser unterstützt keine Spracherkennung.'
                : 'Mikrofon nicht verfügbar.'}
              <button onClick={voice.clearMicError} style={{ marginLeft: 8, background: 'none', border: 'none', color: 'var(--danger)', cursor: 'pointer', textDecoration: 'underline' }}>OK</button>
            </div>
          )}

          {/* Padding für fixed Stop-Button auf Mobile */}
          {isMobile && recording && <div style={{ height: 80, flexShrink: 0 }} />}
        </div>

      /* ── Normal-Modus: Sub-Tabs ── */
      ) : (
        <div className="appear" style={{ maxWidth: isMobile ? '100%' : 680, margin: '0 auto', width: '100%' }}>
          {/* Ortstermin-Datum (aus Diktat extrahiert) */}
          {/* Ortstermin-Eckdaten: Datum + Innenbesichtigung gebündelt in einer
              kompakten Zeile, die sich an ihren Inhalt anpasst (kein full-width
              Kasten mit Leerraum). Adresse steht bereits im Kopf-Titel. */}
          <div style={{
            display: 'inline-flex', alignItems: 'center', gap: 10, flexWrap: 'wrap',
            padding: '7px 12px', marginBottom: 'var(--space-4)',
            background: 'var(--surface-light)', border: '1px solid var(--border-light)',
            borderRadius: 'var(--radius-md)', fontSize: 13, maxWidth: '100%',
          }}>
            {/* Termin-Datum/-Uhrzeit — direkt editierbar (auch wenn nicht aus dem Diktat extrahiert) */}
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
              <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, color: 'var(--text-secondary)' }}>
                <IconClock size={15} />
                <span>Termin</span>
              </span>
              <input
                type="date"
                value={g.ortstermin_datum || ''}
                onChange={async (e) => {
                  if (!g.id) return;
                  try {
                    await apiPatchRow('gutachten', g.id, { ortstermin_datum: e.target.value || null }, session, workerUrl);
                    onRefresh && onRefresh();
                  } catch (err) { showToast('Speichern fehlgeschlagen: ' + err.message, 'error'); }
                }}
                style={{ minHeight: 30, padding: '0 8px', fontSize: 12, fontWeight: 600,
                         color: 'var(--text-primary)', border: '1px solid var(--border-light)',
                         borderRadius: 'var(--radius-sm)', background: 'var(--surface)', cursor: 'pointer', fontFamily: 'inherit' }}
                title="Datum des Ortstermins"
              />
              <input
                type="time"
                value={g.ortstermin_uhrzeit ? g.ortstermin_uhrzeit.substring(0, 5) : ''}
                onChange={async (e) => {
                  if (!g.id) return;
                  try {
                    await apiPatchRow('gutachten', g.id, { ortstermin_uhrzeit: e.target.value || null }, session, workerUrl);
                    onRefresh && onRefresh();
                  } catch (err) { showToast('Speichern fehlgeschlagen: ' + err.message, 'error'); }
                }}
                style={{ minHeight: 30, padding: '0 8px', fontSize: 12,
                         color: 'var(--text-primary)', border: '1px solid var(--border-light)',
                         borderRadius: 'var(--radius-sm)', background: 'var(--surface)', cursor: 'pointer', fontFamily: 'inherit' }}
                title="Uhrzeit (optional)"
              />
            </span>
            <span style={{ width: 1, height: 14, background: 'var(--border-light)' }} />
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
              <span style={{ color: 'var(--text-secondary)' }}>Innenbesichtigung</span>
              <select
                value={g.innenbesichtigung === false ? 'nein' : 'ja'}
                onChange={async (e) => {
                  if (!g.id) return;
                  try {
                    await apiPatchRow('gutachten', g.id, { innenbesichtigung: e.target.value === 'ja' }, session, workerUrl);
                    onRefresh && onRefresh();
                  } catch (err) { showToast('Speichern fehlgeschlagen: ' + err.message, 'error'); }
                }}
                style={{ minHeight: 30, padding: '0 8px', fontSize: 12,
                         border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
                         background: 'var(--surface)', cursor: 'pointer' }}
                title="Bestimmt den Besichtigungs-Text im Export (Kap. 1)"
              >
                <option value="ja">Ja — von innen und außen besichtigt</option>
                <option value="nein">Nein — nur von außen besichtigt</option>
              </select>
            </span>
          </div>

          {/* Begehungs-Fortschritt — Bereiche / Notizen / Fotos auf einen Blick */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)', flexWrap: 'wrap', padding: '12px 16px', marginBottom: 'var(--space-4)', background: 'var(--surface)', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-subtle)' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <span style={{ position: 'relative', width: 36, height: 36, flexShrink: 0 }} title={`${bereicheDone}/${bereicheTotal} Bereiche`}>
                <svg width="36" height="36" viewBox="0 0 36 36" style={{ transform: 'rotate(-90deg)' }}>
                  <circle cx="18" cy="18" r="15" fill="none" stroke="var(--border-light)" strokeWidth="3.5" />
                  <circle cx="18" cy="18" r="15" fill="none" stroke={begehungColor} strokeWidth="3.5" strokeLinecap="round"
                    strokeDasharray={begehungC} strokeDashoffset={begehungC * (1 - begehungPct)}
                    style={{ transition: 'stroke-dashoffset var(--dur-slow) var(--ease-out), stroke var(--dur-base)' }} />
                </svg>
                <span style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, color: begehungColor, fontVariantNumeric: 'tabular-nums' }}>{bereicheDone}/{bereicheTotal}</span>
              </span>
              <div style={{ display: 'flex', flexDirection: 'column' }}>
                <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>Bereiche</span>
                <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                  {bereicheTotal > 0 && bereicheDone >= bereicheTotal ? 'alle erfasst' : `${bereicheDone} von ${bereicheTotal} erfasst`}
                </span>
              </div>
            </div>
            <span style={{ width: 1, height: 32, background: 'var(--border-light)' }} />
            <div style={{ display: 'flex', flexDirection: 'column' }}>
              <span style={{ fontSize: 18, fontWeight: 700, color: 'var(--text-primary)', fontVariantNumeric: 'tabular-nums', lineHeight: 1.1 }}>{notizenCount}</span>
              <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>{notizenCount === 1 ? 'Notiz' : 'Notizen'}</span>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column' }}>
              <span style={{ fontSize: 18, fontWeight: 700, color: 'var(--text-primary)', fontVariantNumeric: 'tabular-nums', lineHeight: 1.1 }}>{fotosCount}</span>
              <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>{fotosCount === 1 ? 'Foto' : 'Fotos'}</span>
            </div>
          </div>

          {/* Sub-Tab Navigation — Apple Segmented Control */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)', marginBottom: 'var(--space-4)', flexWrap: 'wrap' }}>
            <div className="seg" role="tablist" aria-label="Ortstermin-Ansicht">
              {[
                { id: 'aufnahme', label: 'Aufnahme' },
                { id: 'notizen', label: `Notizen${notizenCount ? ` (${notizenCount})` : ''}` },
              ].map(tab => (
                <button
                  key={tab.id}
                  role="tab"
                  aria-selected={subTab === tab.id}
                  className={subTab === tab.id ? 'is-active' : ''}
                  onClick={() => setSubTab(tab.id)}
                  style={{ minHeight: isMobile ? 38 : 34, fontSize: isMobile ? 14 : 13 }}
                >
                  {tab.label}
                </button>
              ))}
            </div>
            {/* Sync-Status */}
            {!isMobile && (
              <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 }}>
                <span style={{
                  fontSize: 10, fontWeight: 600, padding: '2px 6px', borderRadius: 99,
                  background: offline.isOnline ? 'var(--success-bg)' : 'rgba(234,179,8,0.1)',
                  color: offline.isOnline ? 'var(--success)' : '#B45309',
                  display: 'inline-flex', alignItems: 'center', gap: 3,
                }}>
                  <span style={{ width: 5, height: 5, borderRadius: '50%', background: offline.isOnline ? 'var(--success)' : '#EAB308' }} />
                  {offline.isOnline ? 'Online' : 'Offline'}
                </span>
                {offline.pendingCount > 0 && (
                  <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>{offline.pendingCount} pending</span>
                )}
              </div>
            )}
          </div>

          {/* Mobile: Sync-Status unter Tabs */}
          {isMobile && (offline.pendingCount > 0 || !offline.isOnline) && (
            <div style={{
              display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8,
              fontSize: 11, color: offline.isOnline ? 'var(--success)' : '#B45309',
            }}>
              <span style={{ width: 6, height: 6, borderRadius: '50%', background: offline.isOnline ? 'var(--success)' : '#EAB308' }} />
              {offline.isOnline ? 'Online' : 'Offline'}
              {offline.pendingCount > 0 && ` · ${offline.pendingCount} nicht synchronisiert`}
            </div>
          )}

          {/* ── SUB-TAB: Aufnahme ── */}
          {subTab === 'aufnahme' && (
            <div style={{ display: 'flex', flexDirection: 'column' }}>
              {/* Reihenfolge per CSS order: Aufnahme + Foto zuerst (order 0),
                  Bereiche/Vorbereitung darunter (order 2). So bleibt die Logik
                  unverändert, nur die visuelle Anordnung ändert sich. */}
              {/* Vorbereitung + Bereiche — per order:2 unter den Aufnahme-Bereich gerückt */}
              <div style={{ order: 2 }}>
              {/* Vorbereitung — eingeklappt sobald Notizen existieren (Termin läuft).
                  Bereiche-Pills hier ausgeblendet (hideBereiche), da sie als
                  Live-Fortschritt direkt darunter stehen. */}
              <OrtsterminVorbereitung
                p={p} g={g} notes={notes}
                hideBereiche
                defaultCollapsed
              />

              {/* Bereiche — eingeklappte Zusammenfassung, Chips auf Klick (Politur).
                  Vereinheitlicht Mobile + Desktop; ETW-Gruppierung bleibt erhalten. */}
              {(() => {
                const alleErfasst = bereicheStatus.length > 0 && bereicheDone === bereicheStatus.length;
                const hatEigentum = bereicheStatus.some(b => b.eigentum);
                const renderPill = (b) => (
                  <button key={b.id} type="button" onClick={() => springeZuBereichNotizen(b.id)}
                    title={b.done ? `${b.count || 0} Notiz${(b.count || 0) === 1 ? '' : 'en'} — anzeigen` : 'Noch offen'}
                    style={{
                      fontSize: 11, padding: '3px 9px', borderRadius: 99,
                      background: b.done ? 'var(--success-bg)' : 'var(--surface-light)',
                      color: b.done ? 'var(--success)' : 'var(--text-tertiary)',
                      border: `1px solid ${b.done ? 'rgba(22,163,74,0.3)' : 'var(--border-light)'}`,
                      fontWeight: b.done ? 600 : 400, cursor: 'pointer',
                      display: 'inline-flex', alignItems: 'center', gap: 5, fontFamily: 'inherit',
                    }}>
                    {b.done ? '✓' : '○'} {b.label}{b.count > 0 ? ` · ${b.count}` : ''}
                  </button>
                );
                const ge = bereicheStatus.filter(b => b.eigentum === 'ge');
                const se = bereicheStatus.filter(b => b.eigentum === 'se');
                const rest = bereicheStatus.filter(b => !b.eigentum);
                const GroupLabel = ({ children }) => (
                  <span style={{
                    fontSize: 10, fontWeight: 700, textTransform: 'uppercase',
                    letterSpacing: '0.04em', color: 'var(--text-tertiary)',
                    alignSelf: 'center', marginRight: 4,
                  }}>{children}</span>
                );
                return (
                  <div style={{ marginBottom: 'var(--space-4)' }}>
                    {/* Klickbare Zusammenfassungs-Zeile (immer sichtbar) */}
                    <div
                      onClick={() => setBereicheExpanded(v => !v)}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 'var(--space-3)',
                        padding: '11px 14px', cursor: 'pointer',
                        background: 'var(--surface-light)', border: '1px solid var(--border-light)',
                        borderRadius: 'var(--radius-md)',
                      }}
                      title={bereicheExpanded ? 'Bereiche verbergen' : 'Bereiche anzeigen'}
                    >
                      <IconRoute size={16} />
                      <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>Bereiche</span>
                      <span style={{ fontSize: 12, color: alleErfasst ? 'var(--success)' : 'var(--text-tertiary)' }}>
                        {bereicheDone} von {bereicheStatus.length} erfasst
                      </span>
                      <span style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--vl-blue)', display: 'inline-flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap' }}>
                        {bereicheExpanded ? 'verbergen' : 'anzeigen'}
                        <span style={{ transform: bereicheExpanded ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s', display: 'inline-flex' }}>
                          <IconChevronDown size={14} />
                        </span>
                      </span>
                    </div>
                    {/* Aufklappbare Chips */}
                    {bereicheExpanded && (
                      <div style={{ marginTop: 'var(--space-3)' }}>
                        {!hatEigentum ? (
                          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                            {bereicheStatus.map(renderPill)}
                          </div>
                        ) : (
                          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                            {ge.length > 0 && (
                              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
                                <GroupLabel>Gemeinschaftseigentum</GroupLabel>
                                {ge.map(renderPill)}
                              </div>
                            )}
                            {se.length > 0 && (
                              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
                                <GroupLabel>Sondereigentum</GroupLabel>
                                {se.map(renderPill)}
                              </div>
                            )}
                            {rest.length > 0 && (
                              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
                                {rest.map(renderPill)}
                              </div>
                            )}
                          </div>
                        )}
                      </div>
                    )}
                  </div>
                );
              })()}
              </div>

              {/* Objekt-Auswahl (nur bei mehreren Bewertungsobjekten) */}
              {objekte.length > 1 && (
                <div style={{
                  display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center',
                  marginBottom: 'var(--space-3)',
                  padding: '10px 12px', background: 'var(--surface-light)',
                  borderRadius: 'var(--radius-sm)', border: '1px solid var(--border-light)',
                }}>
                  <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginRight: 4 }}>
                    Objekt:
                  </span>
                  {objekte.map(obj => (
                    <button
                      key={obj.id}
                      onClick={() => setSelectedObjektId(obj.id)}
                      style={{
                        fontSize: isMobile ? 13 : 12, padding: isMobile ? '8px 14px' : '5px 12px',
                        borderRadius: 99, cursor: 'pointer',
                        border: selectedObjektId === obj.id
                          ? '1.5px solid var(--vl-blue)' : '1px solid var(--border-light)',
                        background: selectedObjektId === obj.id
                          ? 'rgba(37,99,235,0.08)' : 'var(--surface)',
                        color: selectedObjektId === obj.id
                          ? 'var(--vl-blue)' : 'var(--text-secondary)',
                        fontWeight: selectedObjektId === obj.id ? 600 : 400,
                        minHeight: isMobile ? 40 : 32,
                      }}
                    >
                      {obj.bezeichnung || obj.objekttyp || 'Objekt'}
                    </button>
                  ))}
                  {!selectedObjektId && (
                    <span style={{ fontSize: 11, color: 'var(--danger)', marginLeft: 4 }}>
                      Bitte Objekt wählen
                    </span>
                  )}
                </div>
              )}

              {/* ── Aufnahme-Bereich (Breite kommt vom äußeren Spalten-Wrapper) ── */}
              <div>
                {/* Aufnahme-Hauptaktion + ToDo als gewichtetes Paar */}
                <div style={{ marginBottom: 'var(--space-3)', display: 'flex', gap: 'var(--space-3)' }}>
                  <button
                    className="btn btn-primary"
                    onClick={() => startRecording(false)}
                    style={{
                      flex: '1 1 auto',
                      padding: '0 24px',
                      fontSize: isMobile ? 16 : 15, fontWeight: 600,
                      display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
                      minHeight: 56,
                    }}
                  >
                    <IconMic size={isMobile ? 22 : 20} />
                    Aufnahme starten
                  </button>
                  <button
                    className="btn btn-secondary"
                    onClick={() => startRecording(true)}
                    title="Schnelles ToDo per Sprache — landet als offene Aufgabe in den Aktivitäten"
                    style={{
                      flex: '0 0 auto', minWidth: isMobile ? 96 : 120,
                      padding: '0 20px',
                      fontSize: isMobile ? 14 : 14, fontWeight: 600,
                      display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
                      minHeight: 56,
                    }}
                  >
                    <IconCheck size={18} /> ToDo
                  </button>
                </div>
                <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 'var(--space-5)', textAlign: 'center', lineHeight: 1.6 }}>
                  {isMobile
                    ? 'Kurze Notiz oder ganzer Rundgang — einfach sprechen.'
                    : 'Kurze Notiz oder ganzer Rundgang — einfach sprechen. Bereiche werden automatisch erkannt; längere Aufnahmen werden zur Prüfung aufgeteilt. Mit „ToDo" sprichst du einen offenen Punkt ein.'}
                  {' · '}
                  {/* Notiz importieren — dezenter Link am Hilfetext (verwandt mit der Aufnahme).
                      Genau EINMAL gerendert (importMenuRef für Klick-außerhalb). */}
                  <span ref={importMenuRef} style={{ position: 'relative', display: 'inline-block' }}>
                    <button
                      onClick={() => setImportMenuOpen(o => !o)}
                      disabled={!!audioUploadState || mode === 'processing'}
                      style={{
                        background: 'none', border: 'none', padding: 0, cursor: 'pointer',
                        fontSize: 11, color: 'var(--vl-blue)', fontFamily: 'inherit',
                        display: 'inline-flex', alignItems: 'center', gap: 3, verticalAlign: 'baseline',
                      }}
                      title="Notiz aus Audio, Text oder Handschrift importieren"
                    >
                      oder Notiz importieren
                      <span style={{ fontSize: 9, opacity: 0.7 }}>{importMenuOpen ? '▴' : '▾'}</span>
                    </button>
                    {importMenuOpen && (
                      <div style={{
                        position: 'absolute', top: 'calc(100% + 6px)', left: '50%', transform: 'translateX(-50%)',
                        zIndex: 50, width: 280, maxWidth: '90vw',
                        background: 'var(--surface)', border: '1px solid var(--border-light)',
                        borderRadius: 'var(--radius-md)', boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
                        overflow: 'hidden', textAlign: 'left',
                      }}>
                        <button
                          onClick={() => { setImportMenuOpen(false); audioInputRef.current?.click(); }}
                          style={importMenuItemStyle}
                          onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-light)'}
                          onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
                        >
                          <IconUpload size={18} />
                          <span>
                            <span style={{ display: 'block', fontWeight: 600, fontSize: 13 }}>Audio hochladen</span>
                            <span style={{ display: 'block', fontSize: 11, color: 'var(--text-tertiary)' }}>Aufnahme von einem externen Gerät</span>
                          </span>
                        </button>
                        <button
                          onClick={() => { setImportMenuOpen(false); setShowTranscriptImport(true); }}
                          style={importMenuItemStyle}
                          onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-light)'}
                          onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
                        >
                          <IconDocument size={18} />
                          <span>
                            <span style={{ display: 'block', fontWeight: 600, fontSize: 13 }}>Text importieren</span>
                            <span style={{ display: 'block', fontSize: 11, color: 'var(--text-tertiary)' }}>Bereits transkribierten Text einfügen</span>
                          </span>
                        </button>
                        <button
                          onClick={() => { setImportMenuOpen(false); setHandschriftOpen(true); }}
                          style={{ ...importMenuItemStyle, borderBottom: 'none' }}
                          onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-light)'}
                          onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
                        >
                          <IconCamera size={18} />
                          <span>
                            <span style={{ display: 'block', fontWeight: 600, fontSize: 13 }}>Handschrift abfotografieren</span>
                            <span style={{ display: 'block', fontSize: 11, color: 'var(--text-tertiary)' }}>Wird transkribiert und einsortiert</span>
                          </span>
                        </button>
                      </div>
                    )}
                  </span>
                </div>

                {/* versteckte File-Inputs */}
                <input ref={fileInputRef} type="file" accept="image/*" capture="environment" style={{ display: 'none' }} onChange={handlePhotoCapture} />
                <input ref={galleryInputRef} type="file" accept="image/*" multiple style={{ display: 'none' }} onChange={handlePhotoCapture} />
                <input ref={audioInputRef} type="file" accept="audio/*,.mp3,.m4a,.wav,.ogg,.webm,.aac,.flac" style={{ display: 'none' }} onChange={handleAudioUpload} />

                {/* Foto-Buttons (nur Mobile — Desktop nutzt die Dropzone darunter).
                    Bereich-Zuordnung erfolgt im Overlay nach dem Upload.
                    „Notiz importieren" hängt jetzt am Hilfetext darüber. */}
                {isMobile && (
                  <div style={{ display: 'flex', gap: 0, alignItems: 'stretch', marginTop: 'var(--space-3)' }}>
                    <button
                      className="btn btn-ghost"
                      onClick={() => fileInputRef.current?.click()}
                      title="Foto aufnehmen (Kamera)"
                      style={{
                        display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                        fontSize: 13, minHeight: 44, flex: '1 1 auto',
                        borderTopRightRadius: 0, borderBottomRightRadius: 0,
                        borderRight: 'none',
                      }}
                    >
                      <IconCamera size={16} /> Foto
                    </button>
                    <button
                      className="btn btn-ghost"
                      onClick={() => galleryInputRef.current?.click()}
                      title="Mehrere Fotos aus der Galerie auswählen"
                      style={{
                        display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                        fontSize: 13, minHeight: 44, flex: '1 1 auto',
                      }}
                    >
                      <IconUpload size={16} /> Galerie
                    </button>
                  </div>
                )}

                {/* Drag&Drop-Zone (nur Desktop) — primärer Weg zum Foto-Hinzufügen.
                    Beim Ortstermin zentral, daher prominent gestaltet. */}
                {!isMobile && (
                  <div
                    onDragOver={e => { e.preventDefault(); if (!isDraggingPhoto) setIsDraggingPhoto(true); }}
                    onDragLeave={e => { e.preventDefault(); if (e.currentTarget === e.target) setIsDraggingPhoto(false); }}
                    onDrop={e => {
                      e.preventDefault();
                      setIsDraggingPhoto(false);
                      if (e.dataTransfer?.files?.length) processPhotoFiles(e.dataTransfer.files);
                    }}
                    onClick={() => galleryInputRef.current?.click()}
                    style={{
                      marginTop: 'var(--space-3)', padding: '32px 20px', cursor: 'pointer',
                      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8,
                      border: `2px dashed ${isDraggingPhoto ? 'var(--vl-orange)' : 'var(--border-light)'}`,
                      borderRadius: 'var(--radius-md)', textAlign: 'center',
                      background: isDraggingPhoto ? 'var(--vl-orange-bg, #FEF3E2)' : 'var(--surface-light)',
                      color: isDraggingPhoto ? 'var(--vl-orange-dark, #B45309)' : 'var(--text-secondary)',
                      transition: 'all 0.15s ease',
                    }}
                  >
                    <div style={{
                      width: 44, height: 44, borderRadius: '50%',
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      background: isDraggingPhoto ? 'rgba(250,128,39,0.15)' : 'var(--surface)',
                      color: isDraggingPhoto ? 'var(--vl-orange)' : 'var(--vl-blue)',
                    }}>
                      <IconCamera size={24} />
                    </div>
                    <div style={{ fontSize: 14, fontWeight: 600, color: isDraggingPhoto ? 'var(--vl-orange-dark, #B45309)' : 'var(--text-primary)' }}>
                      {isDraggingPhoto ? 'Fotos hier ablegen…' : 'Fotos hinzufügen'}
                    </div>
                    <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                      Hierher ziehen oder klicken zum Auswählen
                    </div>
                  </div>
                )}

                {/* Nachträgliche Zuordnung: nur zeigen, wenn schon Fotos da sind */}
                {notes.some(n => n.type === 'photo' && n.image_url) && (
                  <button
                    onClick={oeffneNachtraeglicheZuordnung}
                    className="btn btn-ghost btn-sm"
                    style={{ marginTop: 'var(--space-2)', fontSize: 12, color: 'var(--text-secondary)' }}
                    title="Bereits hochgeladene Fotos einem Bereich (neu) zuordnen"
                  >
                    Fotos neu zuordnen
                  </button>
                )}
              </div>

              {/* Foto-Zuordnungs-Overlay */}
              {zuordnungOverlay && (
                <FotoZuordnungOverlay
                  items={zuordnungOverlay.items}
                  areas={PHOTO_AREAS}
                  saving={zuordnungSaving}
                  isMobile={isMobile}
                  onConfirm={(items) => {
                    if (zuordnungOverlay.modus === 'nachtraeglich') {
                      speichereNachtraeglicheZuordnung(items);
                    } else {
                      speichereZuordnung(items);
                    }
                  }}
                  onCancel={() => {
                    if (zuordnungSaving) return;
                    // Vorschau-URLs freigeben (nur Upload-Modus hat Blob-URLs)
                    if (zuordnungOverlay.modus === 'upload') {
                      zuordnungOverlay.items.forEach(it => { try { URL.revokeObjectURL(it.previewUrl); } catch {} });
                    }
                    setZuordnungOverlay(null);
                  }}
                />
              )}

              {/* Phase 5c: Transkript-Import-Modal */}
              {showTranscriptImport && (
                <TranscriptImportModal
                  isMobile={isMobile}
                  onCancel={() => setShowTranscriptImport(false)}
                  onImport={handleTranscriptImport}
                />
              )}

              {/* Rückfrage: ToDo-Diktat wirkt wie Befund → Befund-Notiz oder Aufgabe?
                  Verhindert, dass Befunde versehentlich als Aufgaben in den Aktivitäten landen. */}
              {todoDecision && (
                <div className="modal-backdrop">
                  <div className="modal" style={{ maxWidth: 480 }}>
                    <div className="modal-header">
                      <div className="modal-title">Befund oder Aufgabe?</div>
                    </div>
                    <div className="modal-body" style={{ padding: 'var(--space-5)' }}>
                      <div style={{ fontSize: 14, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
                        Diese Aufnahme klingt nach einem <strong>Befund</strong>, nicht nach einem kurzen ToDo. Wie soll sie gespeichert werden?
                      </div>
                      <div style={{ marginTop: 'var(--space-3)', padding: '10px 12px', background: 'var(--surface-light)', borderRadius: 'var(--radius-sm)', fontSize: 13, color: 'var(--text-primary)', lineHeight: 1.5, maxHeight: 140, overflowY: 'auto' }}>
                        {todoDecision.text}
                      </div>
                    </div>
                    <div className="modal-footer" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
                      <button className="btn btn-ghost" onClick={() => setTodoDecision(null)} style={{ color: 'var(--text-tertiary)' }}>Verwerfen</button>
                      <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
                        <button className="btn btn-secondary" onClick={async () => { const t = todoDecision.text; setTodoDecision(null); await persistTodo(t); }}>Als Aufgabe</button>
                        <button className="btn btn-primary" onClick={async () => { const t = todoDecision.text; setTodoDecision(null); await persistBefundNote(t); }}>Als Befund-Notiz</button>
                      </div>
                    </div>
                  </div>
                </div>
              )}

              {/* Handschrift-OCR Sammel-Modal */}
              {handschriftOpen && (
                <div style={{
                  position: 'fixed', inset: 0, zIndex: 1000,
                  background: 'rgba(0,0,0,0.5)', display: 'flex',
                  alignItems: isMobile ? 'flex-end' : 'center', justifyContent: 'center',
                  padding: isMobile ? 0 : 'var(--space-4)',
                }}
                  onClick={() => { if (!handschriftBusy) setHandschriftOpen(false); }}
                >
                  <div
                    onClick={e => e.stopPropagation()}
                    style={{
                      background: 'var(--surface)', borderRadius: isMobile ? '16px 16px 0 0' : 'var(--radius-lg)',
                      width: '100%', maxWidth: 560, maxHeight: isMobile ? '90vh' : '85vh',
                      display: 'flex', flexDirection: 'column', overflow: 'hidden',
                    }}
                  >
                    {/* Header */}
                    <div style={{ padding: 'var(--space-4) var(--space-5)', borderBottom: '1px solid var(--border-light)' }}>
                      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                        <span style={{ fontSize: 16, fontWeight: 700 }}>Handschrift abfotografieren</span>
                        {!handschriftBusy && (
                          <button onClick={() => setHandschriftOpen(false)}
                            style={{ background: 'none', border: 'none', fontSize: 22, lineHeight: 1, cursor: 'pointer', color: 'var(--text-tertiary)' }}>×</button>
                        )}
                      </div>
                      <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 4, lineHeight: 1.4 }}>
                        Fotografiere deine handschriftlichen Notizen (mehrere Seiten möglich). Sie werden transkribiert, automatisch einsortiert und die Originalfotos als Beleg gespeichert.
                      </div>
                    </div>

                    {/* Foto-Grid */}
                    <div style={{ padding: 'var(--space-4) var(--space-5)', overflow: 'auto', flex: 1 }}>
                      <input ref={handschriftInputRef} type="file" accept="image/*" capture="environment" multiple
                        style={{ display: 'none' }} onChange={handleHandschriftAdd} />
                      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(90px, 1fr))', gap: 8 }}>
                        {handschriftFotos.map((foto, i) => (
                          <div key={foto.id} style={{ position: 'relative', aspectRatio: '3/4', borderRadius: 'var(--radius-sm)', overflow: 'hidden', border: '1px solid var(--border-light)' }}>
                            <img src={foto.previewUrl} alt={`Seite ${i+1}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                            <div style={{ position: 'absolute', top: 2, left: 4, fontSize: 10, fontWeight: 700, color: '#fff', textShadow: '0 1px 2px rgba(0,0,0,0.8)' }}>{i+1}</div>
                            {!handschriftBusy && (
                              <button onClick={() => handleHandschriftRemove(foto.id)}
                                style={{ position: 'absolute', top: 2, right: 2, width: 22, height: 22, borderRadius: '50%', border: 'none', background: 'rgba(0,0,0,0.6)', color: '#fff', fontSize: 14, lineHeight: 1, cursor: 'pointer' }}>×</button>
                            )}
                          </div>
                        ))}
                        {!handschriftBusy && handschriftFotos.length < 10 && (
                          <button onClick={() => handschriftInputRef.current?.click()}
                            style={{
                              aspectRatio: '3/4', borderRadius: 'var(--radius-sm)',
                              border: '2px dashed var(--border-medium)', background: 'var(--surface-light)',
                              display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
                              gap: 6, cursor: 'pointer', color: 'var(--text-tertiary)', fontSize: 12,
                            }}>
                            <IconCamera size={22} />
                            {handschriftFotos.length === 0 ? 'Foto' : '+ Seite'}
                          </button>
                        )}
                      </div>
                      {handschriftBusy && (
                        <div style={{ textAlign: 'center', marginTop: 'var(--space-4)', fontSize: 13, color: 'var(--vl-blue)', fontWeight: 600 }}>
                          {handschriftBusy === 'ocr' ? 'Handschrift wird gelesen…' : 'Notizen werden einsortiert…'}
                        </div>
                      )}
                    </div>

                    {/* Footer */}
                    <div style={{ padding: 'var(--space-4) var(--space-5)', borderTop: '1px solid var(--border-light)', display: 'flex', gap: 'var(--space-3)', justifyContent: 'flex-end' }}>
                      <button className="btn btn-ghost" onClick={() => setHandschriftOpen(false)} disabled={!!handschriftBusy}>Abbrechen</button>
                      <button className="btn btn-primary" onClick={handleHandschriftTranscribe}
                        disabled={handschriftFotos.length === 0 || !!handschriftBusy}>
                        {handschriftBusy ? 'Verarbeite…' : `${handschriftFotos.length || ''} ${handschriftFotos.length === 1 ? 'Seite' : 'Seiten'} transkribieren`}
                      </button>
                    </div>
                  </div>
                </div>
              )}

              {/* Audio-Upload Progress-Overlay */}
              {audioUploadState && (
                <div style={{
                  background: 'var(--surface)', border: '1px solid var(--border-light)',
                  borderRadius: 'var(--radius-md)', padding: 'var(--space-4)',
                  marginBottom: 'var(--space-4)',
                }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
                    <span style={{ fontWeight: 600, fontSize: 13 }}>Audio-Transkription</span>
                    {audioUploadState.totalChunks > 0 && (
                      <span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
                        {audioUploadState.chunk}/{audioUploadState.totalChunks}
                      </span>
                    )}
                  </div>
                  <div style={{ height: 4, background: 'var(--border-light)', borderRadius: 2, marginBottom: 8, overflow: 'hidden' }}>
                    <div style={{
                      height: '100%', borderRadius: 2,
                      background: audioUploadState.phase === 'correcting' ? 'var(--vl-orange)' : 'var(--vl-blue)',
                      transition: 'width 0.4s ease',
                      width: audioUploadState.totalChunks > 0
                        ? `${Math.round((audioUploadState.chunk / audioUploadState.totalChunks) * 100)}%`
                        : '20%',
                      animation: audioUploadState.totalChunks === 0 ? 'pulse 1.5s infinite' : 'none',
                    }} />
                  </div>
                  <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: audioUploadState.text ? 8 : 0 }}>
                    {audioUploadState.message}
                  </div>
                  {audioUploadState.text && (
                    <div style={{
                      fontSize: 12, color: 'var(--text-primary)', lineHeight: 1.5,
                      maxHeight: 120, overflow: 'auto',
                      padding: '8px 10px', background: 'var(--surface-light)',
                      borderRadius: 'var(--radius-sm)', border: '1px solid var(--border-light)',
                    }}>
                      {audioUploadState.text.length > 300
                        ? '...' + audioUploadState.text.slice(-300)
                        : audioUploadState.text}
                    </div>
                  )}
                </div>
              )}
            </div>
          )}

          {/* ── SUB-TAB: Notizen ── */}
          {subTab === 'notizen' && (
            <div className="card">
              <div className="card-header">
                <span className="card-title">Notizen ({notes.filter(n => n.type !== 'photo' && n.kapitel !== 'transcript_raw').length})</span>
                {offline.transcribeQueue > 0 && offline.isOnline && (
                  <button className="btn btn-ghost btn-sm" onClick={() => offline.processTranscribeQueue(workerUrl, session)}
                    style={{ fontSize: 11, color: 'var(--vl-orange)' }}>
                    {offline.transcribeQueue} Audio transkribieren
                  </button>
                )}
              </div>
              <div style={{ padding: 'var(--space-4)' }}>
                {notesLoading ? (
                  <div style={{ textAlign: 'center', color: 'var(--text-tertiary)', padding: 'var(--space-5)' }}>Lade…</div>
                ) : notes.length === 0 ? (
                  <div style={{ textAlign: 'center', color: 'var(--text-tertiary)', padding: 'var(--space-5)', fontSize: 14 }}>
                    Noch keine Notizen. Wechsle zu "Aufnahme" um loszulegen.
                  </div>
                ) : (
                  <>
                    {kapitelList.map(kap => (
                      <div key={kap.id} id={`notiz-kap-${kap.id}`} style={{ marginBottom: 'var(--space-4)' }}>
                        <div style={{
                          fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
                          letterSpacing: '0.08em', color: 'var(--text-secondary)',
                          marginBottom: 'var(--space-2)',
                        }}>
                          {kap.label} ({notesByKapitel[kap.id].length})
                        </div>
                        {notesByKapitel[kap.id].map(n => (
                          <NoteCard key={n.id} note={n} session={session} workerUrl={workerUrl}
                            objekte={objekte} onEdit={handleEditNote} onDelete={handleDeleteNote} />
                        ))}
                      </div>
                    ))}
                    {hasUnkategorisiert && (
                      <div>
                        <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-secondary)', marginBottom: 'var(--space-2)' }}>
                          Unkategorisiert ({notesByKapitel['_none'].length})
                        </div>
                        {notesByKapitel['_none'].map(n => (
                          <NoteCard key={n.id} note={n} session={session} workerUrl={workerUrl}
                            objekte={objekte} onEdit={handleEditNote} onDelete={handleDeleteNote} />
                        ))}
                      </div>
                    )}
                    {(notesByKapitel['_transcript_raw'] || []).length > 0 && (
                      <div style={{ marginTop: 'var(--space-4)' }}>
                        <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-tertiary)', marginBottom: 'var(--space-2)' }}>
                          Belege — Original-Transkripte
                        </div>
                        {notesByKapitel['_transcript_raw'].map(n => (
                          <NoteCard key={n.id} note={n} session={session} workerUrl={workerUrl}
                            objekte={objekte} onEdit={handleEditNote} onDelete={handleDeleteNote} />
                        ))}
                      </div>
                    )}
                  </>
                )}
              </div>
            </div>
          )}
        </div>
      )}

      {/* Recording Overlay — unten fixiert während Aufnahme */}
      {recording && (
        <RecordingOverlay
          voice={voice}
          rundgang={rundgang}
          mode={'rundgang'}
          onStop={stopRecording}
          kontextGutachten={g}
          objekte={objekte}
          bereicheStatus={bereicheStatus}
        />
      )}

      <style>{`
        @keyframes pulse {
          0%, 100% { opacity: 1; }
          50% { opacity: 0.6; }
        }
        @keyframes spin {
          from { transform: rotate(0deg); }
          to { transform: rotate(360deg); }
        }
      `}</style>
    </>
  );
};


// ══════════════════════════════════════════════════════════════════
// ENTWURF-TAB: Baustein, Kapitel-Section, Hauptkomponente
// ══════════════════════════════════════════════════════════════════

// ── Einzelner Textbaustein (= ein AI-Tag) ──
// Farb-/Stil-Zuordnung je Herkunfts-Kategorie (dezente Badges)
// Kapitel-Labels für die feingranulare Herkunft (Transparenz Stufe 2).
// Identisch zu den LABELS im Worker (formatNotizen), damit die angezeigte
// Quelle exakt der Notizen-Gruppierung beim Generieren entspricht.
const NOTIZ_KAPITEL_LABELS = {
  aussen: 'Außen / Grundstück', grundstueck: 'Grundstück', gebaeude: 'Gebäude (außen)', dach: 'Dach',
  keller: 'Kellergeschoss', eg: 'Erdgeschoss', og1: '1. Obergeschoss', og2: '2. Obergeschoss',
  dg: 'Dachgeschoss', spitzboden: 'Spitzboden', garage: 'Garage / Stellplatz', technik_heizung: 'Technik: Heizung',
  technik_elektro: 'Technik: Elektro', technik_wasser: 'Technik: Wasser/Sanitär',
  maengel: 'Mängel / Schäden', wohnwert: 'Wohnwertprüfung', beurteilung: 'Beurteilung des Sachverständigen', allgemein: 'Allgemein',
};

const PROVENANCE_STYLE = {
  ortstermin:            { bg: '#FEF3E2', color: '#B45309', kurz: 'Ortstermin' },
  stammdaten:            { bg: '#E0F2FE', color: '#0369A1', kurz: 'Stammdaten' },
  dokument:              { bg: '#E8F5E9', color: '#0D7A4F', kurz: 'Dokument' },
  wissen:                { bg: '#EDE9FE', color: '#6D28D9', kurz: 'KI-Wissen' },
  ortstermin_stammdaten: { bg: '#FEF3E2', color: '#B45309', kurz: 'Ortstermin + Stammdaten' },
  ortstermin_wissen:     { bg: '#FEF3E2', color: '#B45309', kurz: 'Ortstermin + Wissen' },
  stammdaten_wissen:     { bg: '#E0F2FE', color: '#0369A1', kurz: 'Stammdaten + Wissen' },
  gemischt:              { bg: '#F4F6F8', color: '#64748B', kurz: 'Gemischt' },
  manuell:               { bg: '#F4F6F8', color: '#64748B', kurz: 'Manuell' },
  berechnung:            { bg: '#F4F6F8', color: '#64748B', kurz: 'Berechnung' },
  foto:                  { bg: '#F4F6F8', color: '#64748B', kurz: 'Fotos' },
};

const EntwurfBaustein = ({ tagId, output, onSave, onRegenerate, disabled }) => {
  const [editing, setEditing] = useState(false);
  const [editText, setEditText] = useState('');
  const [saving, setSaving] = useState(false);
  const [regenerating, setRegenerating] = useState(false);

  const label = entwurfTagLabel(tagId);
  const status = output?.status || 'pending';
  const value = output?.value || '';
  const statusCfg = ENTWURF_STATUS_CONFIG[status] || ENTWURF_STATUS_CONFIG.pending;
  const isGenerating = status === 'generating' || regenerating;

  // Herkunft (Transparenz): grobe Kategorie aus der Provenance-Map
  const prov = tagProvenanceFor(tagId);
  const provStyle = prov ? (PROVENANCE_STYLE[prov.kategorie] || PROVENANCE_STYLE.gemischt) : null;
  // Bei manuell bearbeiteten Tags: Herkunft = "manuell überschrieben"
  const istManuell = output?.is_manually_edited;
  const provTooltip = prov
    ? (prov.dokumentTypen && prov.dokumentTypen.length
        ? `Herkunft: ${prov.label} (${prov.dokumentTypen.map(dokumenttypLabel).join(', ')})`
        : `Herkunft: ${prov.label}`)
    : '';

  const startEdit = () => { setEditText(value); setEditing(true); };
  const cancelEdit = () => { setEditing(false); setEditText(''); };

  const handleSave = async () => {
    if (editText.trim() === value.trim()) { cancelEdit(); return; }
    setSaving(true);
    try {
      await onSave(tagId, editText.trim());
      setEditing(false);
    } catch (e) {
      alert('Fehler beim Speichern: ' + (e.message || e));
    } finally { setSaving(false); }
  };

  const handleRegenerate = async () => {
    if (output?.is_manually_edited && !confirm('Dieser Baustein wurde manuell bearbeitet. Trotzdem neu generieren?')) return;
    setRegenerating(true);
    try {
      await onRegenerate(tagId, output?.is_manually_edited);
    } catch (e) {
      alert('Fehler: ' + (e.message || e));
    } finally { setRegenerating(false); }
  };

  return (
    <div className="entwurf-baustein" data-status={status}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', marginBottom: 'var(--space-1)' }}>
        <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>{label}</span>
        {prov && provStyle && (
          <span
            title={istManuell ? 'Manuell überschrieben (ursprüngliche Herkunft: ' + prov.label + ')' : provTooltip}
            style={{
              display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 10, fontWeight: 500,
              padding: '1px 6px', borderRadius: 99,
              background: istManuell ? '#F4F6F8' : provStyle.bg,
              color: istManuell ? '#64748B' : provStyle.color,
            }}
          >
            {istManuell ? 'manuell' : provStyle.kurz}
          </span>
        )}
        <span style={{
          display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10, fontWeight: 500,
          padding: '1px 7px', borderRadius: 99, marginLeft: 'auto',
          background: statusCfg.bg, color: statusCfg.color,
        }}>
          <span style={{ width: 6, height: 6, borderRadius: '50%', background: statusCfg.dot, display: 'inline-block' }} />
          {statusCfg.label}
        </span>
      </div>

      {editing ? (
        <div>
          <textarea
            value={editText}
            onChange={e => setEditText(e.target.value)}
            className="entwurf-textarea"
            rows={Math.max(3, Math.ceil((editText.length || 1) / 80))}
            autoFocus
            disabled={saving}
          />
          <div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-2)' }}>
            <button className="btn btn-primary btn-sm" onClick={handleSave} disabled={saving}>
              {saving ? 'Speichert...' : 'Speichern'}
            </button>
            <button className="btn btn-ghost btn-sm" onClick={cancelEdit} disabled={saving}>Abbrechen</button>
          </div>
        </div>
      ) : (
        <>
          <div
            className={`entwurf-baustein-text ${!value ? 'empty' : ''} ${isGenerating ? 'generating' : ''}`}
            onClick={value && !disabled ? startEdit : undefined}
            title={value ? 'Klicken zum Bearbeiten' : undefined}
            style={{ cursor: value && !disabled ? 'text' : 'default' }}
          >
            {isGenerating ? (
              <span style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--vl-blue)', fontSize: 13 }}>
                <span className="entwurf-spinner" /> Wird generiert...
              </span>
            ) : value ? value : (
              <span style={{ fontStyle: 'italic', color: 'var(--text-tertiary)', fontSize: 13 }}>— noch nicht generiert —</span>
            )}
          </div>

          {status === 'error' && output?.error_message && (
            <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4, padding: '4px 8px', background: 'rgba(220,38,38,0.05)', borderRadius: 'var(--radius-sm)' }}>
              {output.error_message}
            </div>
          )}

          {!disabled && (
            <div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-2)' }}>
              {value && (
                <button className="entwurf-action-btn" onClick={startEdit}>Bearbeiten</button>
              )}
              <button className="entwurf-action-btn accent" onClick={handleRegenerate} disabled={isGenerating}>
                {regenerating ? 'Generiert...' : value ? 'Neu generieren' : 'Generieren'}
              </button>
            </div>
          )}
        </>
      )}
    </div>
  );
};


// ── Collapsible Kapitel-Section ──
const EntwurfKapitelSection = ({ kapitel, outputs, expanded, onToggle, onSave, onRegenerate, disabled }) => {
  const stats = computeKapitelStats(kapitel.subkapitel, outputs);
  const [expandedSubs, setExpandedSubs] = useState(() => new Set(kapitel.subkapitel.map(s => s.id)));

  const toggleSub = (subId) => {
    setExpandedSubs(prev => {
      const next = new Set(prev);
      if (next.has(subId)) next.delete(subId); else next.add(subId);
      return next;
    });
  };

  return (
    <div className="entwurf-kapitel">
      <button className="entwurf-kapitel-header" onClick={onToggle}>
        <span style={{ flex: 1, minWidth: 0 }}>{kapitel.label}</span>
        <span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text-tertiary)', fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>
          {stats.done}/{stats.total}
        </span>
        <div style={{ width: 60, height: 4, borderRadius: 2, background: 'var(--border-light)', flexShrink: 0, overflow: 'hidden' }}>
          <div style={{ height: '100%', background: 'var(--success)', borderRadius: 2, width: `${stats.pct}%`, transition: 'width 0.5s ease' }} />
        </div>
        <span className={`entwurf-chevron ${expanded ? 'open' : ''}`}>▾</span>
      </button>

      {expanded && (
        <div style={{ borderTop: '1px solid var(--border-light)', padding: 'var(--space-3) var(--space-5) var(--space-4)' }}>
          {kapitel.subkapitel.map(sub => {
            const subExpanded = expandedSubs.has(sub.id);
            const subDone = sub.aiTags.filter(t => {
              const o = outputs[t];
              return o && (o.status === 'generated' || o.status === 'edited');
            }).length;

            return (
              <div key={sub.id} style={{ marginBottom: 'var(--space-2)' }}>
                <button
                  onClick={() => toggleSub(sub.id)}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 'var(--space-2)', width: '100%',
                    padding: 'var(--space-2) 0', background: 'none', border: 'none', cursor: 'pointer',
                    fontSize: 13, fontWeight: 600, color: 'var(--vl-blue)', textAlign: 'left',
                  }}
                >
                  <span className={`entwurf-chevron small ${subExpanded ? 'open' : ''}`}>▾</span>
                  <span style={{ flex: 1 }}>{sub.label}</span>
                  {sub.isBeurteilung && (
                    <span style={{ fontSize: 10, fontWeight: 600, color: 'var(--vl-orange-dark)', background: 'var(--vl-orange-bg)', padding: '1px 6px', borderRadius: 99 }}>
                      Beurteilung
                    </span>
                  )}
                  <span style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-tertiary)', fontVariantNumeric: 'tabular-nums' }}>
                    {subDone}/{sub.aiTags.length}
                  </span>
                </button>

                {subExpanded && (
                  <div style={{ paddingLeft: 'var(--space-4)', borderLeft: '2px solid var(--border-light)', marginLeft: 6, marginBottom: 'var(--space-3)' }}>
                    {sub.aiTags.map(tagId => (
                      <EntwurfBaustein
                        key={tagId}
                        tagId={tagId}
                        output={outputs[tagId]}
                        onSave={onSave}
                        onRegenerate={onRegenerate}
                        disabled={disabled}
                      />
                    ))}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};


// ══════════════════════════════════════════════════════════════════
// ENTWURF-VORSCHAU — Vollständige Gutachten-Dokumentansicht
//
// Rendert den Entwurf in der echten Gutachten-Kapitelstruktur
// (Kap. 1–12) mit korrekter Nummerierung und Platzhaltern
// für automatisch erzeugte Abschnitte.
// ══════════════════════════════════════════════════════════════════

// ── Abbildungs-Registry ──
// Definiert alle Bild-Slots eines Gutachtens: Typ, Position, Kapitel-Mapping.
// Wird in der Vorschau als Platzhalter gerendert und später für die
// Zuordnung (Fotos-Tab) und den DOCX-Export verwendet.
//
// category:
//   'karte'    — Übersichtskarten, Stadtpläne (meist extern beschafft)
//   'plan'     — Amtliche Pläne, Bebauungsplan, Bodenrichtwertkarte
//   'foto'     — Ortstermin-Fotos (Zuordnung via kapitelMapping)
//   'bauplan'  — Grundrisse, Ansichten aus Bauunterlagen
//
// kapitelMapping: Verbindung zu VL_KAPITEL_TAGS für Auto-Vorschläge
//   aus dem Rundgang-System.

const GUTACHTEN_ABBILDUNGEN = [
  // ─── Deckblatt ───
  { id: 'titelbild', label: 'Titelbild', desc: 'Objektfoto für das Deckblatt', category: 'foto', required: true,
    position: 'deckblatt', kapitelMapping: 'aussen' },

  // ─── Inline im Dokument ───
  { id: 'lagekarte', label: 'Lagekarte', desc: 'Übersichtskarte Makrolage', category: 'karte', required: true,
    position: { after: '3.1' } },
  { id: 'mikrolage_karte', label: 'Mikrolage', desc: 'Stadtteilkarte mit Pfeil', category: 'karte', required: true,
    position: { after: '3.3', before: 'beurteilung_lage' } },
  { id: 'lageplan', label: 'Lageplan', desc: 'Amtlicher Lageplan', category: 'plan', required: true,
    position: { after: '4.1' } },
  { id: 'luftbild', label: 'Luftbild', desc: 'Luftbild des Grundstücks', category: 'karte', required: false,
    position: { after: '4.1', afterSlot: 'lageplan' } },
  { id: 'laerm_lden', label: 'Lärm LDEN', desc: 'Lärmkartierung Tag', category: 'karte', required: false,
    position: { after: '4.3' } },
  { id: 'laerm_lnight', label: 'Lärm LNight', desc: 'Lärmkartierung Nacht', category: 'karte', required: false,
    position: { after: '4.3', afterSlot: 'laerm_lden' } },
  { id: 'bebauungsplan', label: 'Bebauungsplan', desc: 'Auszug B-Plan', category: 'plan', required: true,
    position: { in: '5' } },
  { id: 'bodenrichtwertkarte', label: 'Bodenrichtwertkarte', desc: 'BRW-Karte mit Markierung', category: 'plan', required: false,
    position: { after: '9.1' } },

  // ─── Anlagen: Baupläne ───
  { id: 'bauplan_ansicht_1', label: 'Ansicht 1', desc: 'Nord-/Ostansicht', category: 'bauplan', required: false,
    position: 'anlagen_bauplaene' },
  { id: 'bauplan_ansicht_2', label: 'Ansicht 2', desc: 'Süd-/Westansicht', category: 'bauplan', required: false,
    position: 'anlagen_bauplaene' },
  { id: 'bauplan_kg', label: 'Grundriss KG', desc: 'Kellergeschoss', category: 'bauplan', required: false,
    position: 'anlagen_bauplaene' },
  { id: 'bauplan_eg', label: 'Grundriss EG', desc: 'Erdgeschoss', category: 'bauplan', required: false,
    position: 'anlagen_bauplaene' },
  { id: 'bauplan_dg', label: 'Grundriss DG', desc: 'Dachgeschoss', category: 'bauplan', required: false,
    position: 'anlagen_bauplaene' },
];

// Foto-Kategorien für die Bildliche Darstellungen (Anlagen).
// Jede Kategorie kann beliebig viele Fotos enthalten.
// kapitelId verknüpft mit VL_KAPITEL_TAGS / Rundgang-System.
const FOTO_KATEGORIEN = [
  { id: 'aussen',   label: 'Außenaufnahmen',  kapitelIds: ['aussen','grundstueck','gebaeude','dach','garage'] },
  { id: 'keller',   label: 'Kellergeschoss',  kapitelIds: ['keller'] },
  { id: 'eg',       label: 'Erdgeschoss',     kapitelIds: ['eg'] },
  { id: 'og1',      label: '1. Obergeschoss', kapitelIds: ['og1'] },
  { id: 'dg',       label: 'Dachgeschoss',    kapitelIds: ['dg'] },
  { id: 'sp',       label: 'Spitzboden',      kapitelIds: ['dg'] }, // often grouped with DG
  { id: 'garage',   label: 'Garage',          kapitelIds: ['garage'] },
  { id: 'technik',  label: 'Technik',         kapitelIds: ['technik_heizung','technik_elektro','technik_wasser'] },
  { id: 'maengel',  label: 'Mängel / Schäden',kapitelIds: ['maengel'] },
];

// ── Vollständige Dokumentstruktur ──
// Bildet die echte V&L-Gutachten-Gliederung ab (Kap. 1–12).
// type='auto': Stammdaten/Extraktionen (Platzhalter)
// type='entwurf': aiTags aus dem Entwurf-System
// type='abbildung': Bild-Platzhalter (referenziert GUTACHTEN_ABBILDUNGEN)
// type='anlagen_fotos': Fotodokumentation-Block (referenziert FOTO_KATEGORIEN)
// inlineLabels: Tags als "Label: Text" (Raumbeschreibungen)

const VORSCHAU_DOKUMENT = [
  { nr: '1', label: 'Zusammenfassung der wesentlichen Daten', depth: 1, type: 'auto',
    hint: 'Wird automatisch aus den Stammdaten erzeugt (Auftraggeber, Aktenzeichen, Objektanschrift, Ortstermin, Verkehrswert).' },
  { nr: '2', label: 'Grundbuchdaten', depth: 1, type: 'auto',
    hint: 'Wird aus dem Grundbuchauszug übernommen (Bestandsverzeichnis, Abt. I–III).' },

  // 3. Lage
  { nr: '3', label: 'Lage', depth: 1, type: 'heading', editId: 'lage' },
  { nr: '3.1', label: 'Makrolage', depth: 2, type: 'entwurf', editId: 'lage',
    aiTags: ['makrolage_1','makrolage_2','makrolage_3','makrolage_4','makrolage_5','makrolage_6'] },
  { depth: 2, type: 'abbildung', slotId: 'lagekarte' },
  { nr: '3.2', label: 'Demografische Entwicklung', depth: 2, type: 'entwurf', editId: 'lage',
    aiTags: ['demografie_einleitung'], demografieTabelle: true },
  { nr: '3.3', label: 'Mikrolage', depth: 2, type: 'entwurf', editId: 'lage', subHeadings: true,
    aiTags: ['kleinraeumige_lage','strasse_parkplatz','areal_umgebung','nahversorgung','verkehrsanbindung'] },
  { depth: 2, type: 'abbildung', slotId: 'mikrolage_karte' },
  { nr: null, label: 'Beurteilung', depth: 3, type: 'beurteilung', editId: 'lage',
    aiTags: ['beurteilung_lage','lageurteil'] },

  // 4. Grundstück
  { nr: '4', label: 'Grundstück', depth: 1, type: 'heading', editId: 'grundstueck' },
  { depth: 1, type: 'static', noHeading: true, editId: 'grundstueck',
    text: 'Die nachstehenden Darstellungen und Ausführungen basieren auf den übergebenen/eingeholten Unterlagen, der persönlichen Inaugenscheinnahme des Sachverständigen, den Auskünften der Teilnehmer am Ortstermin sowie dem amtlichen Lageplan. Sie stellen die überwiegenden Grundstücksmerkmale und Eigenschaften des Bewertungsgrundstücks dar und haben somit nicht den Status der Vollständigkeit bzw. des Abschließenden.',
    italic: true },
  { nr: '4.1', label: 'Grundstücksbeschreibung', depth: 2, type: 'entwurf', editId: 'grundstueck', inlineLabels: true, grundstueckStammdaten: true,
    aiTags: ['grundstueckszuschnitt','topographie','hoehenlage','ausrichtung'] },
  { depth: 2, type: 'entwurf', editId: 'grundstueck', noHeading: true, subHeadings: true,
    aiTags: ['angrenzungen','bebauung_grundstueck','zufahrt','zuwegung','einfriedung','freiflaechengestaltung','freiflaechengestaltung_2','freiflaechengestaltung_3'] },
  { depth: 2, type: 'abbildung', slotId: 'lageplan' },
  { depth: 2, type: 'abbildung', slotId: 'luftbild' },
  { nr: '4.2', label: 'Bodenbeschaffenheit', depth: 2, type: 'entwurf', editId: 'grundstueck',
    aiTags: ['altlastenauskunft'] },
  { nr: '4.3', label: 'Umwelteinflüsse, Naturgefahren (Georisiken) und Immissionen', depth: 2, type: 'entwurf', editId: 'grundstueck', subHeadings: true,
    staticIntro: 'Die nachfolgenden Angaben wurden mit der Geoanwendung BayernAtlas der Bayerischen Vermessungsverwaltung überprüft. Es liegen keine weiteren Informationen vor, da keine gesonderten Erhebungen durchgeführt wurden. Auch eigene Geräuschmessungen, etwa aus Flug-, Bahn- oder Kfz-Verkehr, wurden nicht durchgeführt.',
    aiTags: ['umwelteinfluesse','naturgefahren','immissionen'] },
  { depth: 2, type: 'abbildung', slotId: 'laerm_lden' },
  { depth: 2, type: 'abbildung', slotId: 'laerm_lnight' },
  { nr: null, label: 'Beurteilung', depth: 3, type: 'beurteilung', editId: 'grundstueck',
    aiTags: ['beurteilung_grundstueck_1','beurteilung_grundstueck_2','beurteilung_grundstueck_3','beurteilung_grundstueck_4','beurteilung_grundstueck_5'] },

  // 5. Rechtliche Gegebenheiten
  { nr: '5', label: 'Rechtliche Gegebenheiten', depth: 1, type: 'entwurf', editId: 'recht',
    aiTags: ['einleitung_wohnungsgrundbuch','einleitung_teileigentumsgrundbuch'] },
  { nr: null, depth: 1, type: 'entwurf', editId: 'recht', subHeadings: true, noHeading: true,
    aiTags: ['bauplanungsrecht','flaechennutzungsplan','satzungen'] },
  { depth: 1, type: 'abbildung', slotId: 'bebauungsplan' },
  { nr: null, depth: 1, type: 'entwurf', editId: 'recht', subHeadings: true, noHeading: true,
    aiTags: ['erschliessungsbeitraege','herstellungsbeitraege','grundstuecksentwaesserung','gebaeudeversicherung','denkmalschutz','gemeinschaftseigentum','nachtraege_teilungserklaerung','mietvertraege','zubehoer'] },

  // 6. Gebäude
  { nr: '6', label: 'Gebäude', depth: 1, type: 'heading', editId: 'gebaeude' },
  { nr: '6.1', label: 'Einfamilienhaus', depth: 2, type: 'entwurf', editId: 'gebaeude',
    aiTags: ['wohnhaus_einleitung','bewertungseinheit_zuordnung','baujahre','gebaeudeart','hauseingang','vertikale_erschliessung'] },
  { nr: null, label: 'Konstruktiver Aufbau', depth: 3, type: 'entwurf', editId: 'gebaeude',
    aiTags: ['fassade','fenster_allgemein','dach','geschossdecken','heizungsanlage_allgemein'], inlineLabels: true },
  { nr: '6.1.1', label: 'Kellergeschoss', depth: 3, type: 'entwurf', editId: 'gebaeude',
    aiTags: ['kg_raumaufteilung','kg_fussboeden','kg_waende','kg_decken','kg_tueren','kg_fenster','kg_heizungsanlage','kg_beheizung','kg_sonstiges'], inlineLabels: true, introTag: 'kg_raumaufteilung' },
  { nr: '6.1.2', label: 'Erdgeschoss', depth: 3, type: 'entwurf', editId: 'gebaeude',
    aiTags: ['eg_raumaufteilung','eg_fussboeden','eg_waende','eg_decken','eg_tueren','eg_fenster','eg_beheizung','eg_sanitaer','eg_sonstiges'], inlineLabels: true, introTag: 'eg_raumaufteilung' },
  { nr: '6.1.3', label: 'Dachgeschoss', depth: 3, type: 'entwurf', editId: 'gebaeude',
    aiTags: ['dg_raumaufteilung','dg_fussboeden','dg_waende','dg_decken','dg_tueren','dg_fenster','dg_beheizung','dg_sanitaer','dg_sonstiges'], inlineLabels: true, introTag: 'dg_raumaufteilung' },
  { nr: '6.1.4', label: 'Spitzboden', depth: 3, type: 'entwurf', editId: 'gebaeude',
    aiTags: ['sp_zugang','sp_raumaufteilung','sp_fussboeden','sp_schraegen','sp_beheizung','sp_belichtung','sp_sonstiges'], inlineLabels: true },
  { nr: '6.2', label: 'Garage', depth: 2, type: 'entwurf', editId: 'gebaeude',
    aiTags: ['garage_einleitung','garage_ausstattung'] },
  { nr: '6.3', label: 'Sonstige bauliche Besonderheiten', depth: 2, type: 'entwurf', editId: 'gebaeude',
    aiTags: ['energieausweis','schornsteinfeger'] },
  { nr: null, label: 'Beurteilung', depth: 3, type: 'beurteilung', editId: 'gebaeude',
    aiTags: ['beurteilung_gebaeude_1','beurteilung_gebaeude_2','beurteilung_gebaeude_3','beurteilung_gebaeude_4'] },

  // 7. Flächenangaben
  { nr: '7', label: 'Flächenangaben und Berechnungen', depth: 1, type: 'auto',
    hint: 'Wohn- und Nutzflächenberechnungen werden separat erstellt.' },

  // 8. Wirtschaftliche Gegebenheiten
  { nr: '8', label: 'Wirtschaftliche Gegebenheiten und Grundstücksmarkt', depth: 1, type: 'entwurf', editId: 'verfahren',
    aiTags: ['standortbewertung','markteinordnung','marktdynamik','marktzusammenfassung'] },

  // 9. Wertermittlung
  { nr: '9', label: 'Wertermittlung', depth: 1, type: 'entwurf', editId: 'verfahren',
    aiTags: ['gegenstand_wertermittlung','nutzungsperspektive','verfahrensbegruendung','verfahrenswahl'] },
  { nr: '9.1', label: 'Bodenwert', depth: 2, type: 'entwurf', editId: 'verfahren',
    aiTags: ['bodenrichtwerte_beschreibung','grundstuecksdaten_bodenwert','erschliessungsstatus_bodenwert','lagebeurteilung_bodenwert','preisentwicklung_bodenwert','brw_heranziehung'] },
  { depth: 2, type: 'abbildung', slotId: 'bodenrichtwertkarte' },
  { nr: '9.2', label: 'Sachwert', depth: 2, type: 'entwurf', editId: 'verfahren',
    aiTags: ['sachwert_modell','gnd_wohnhaus','gnd_garage','rnd_berechnung','rnd_modifikation','aussenanlagen_ansatz','besondere_bauteile','sachwertfaktor','sachwertfaktor_energetik','sachwertfaktor_preisentwicklung','sachwertfaktor_ergebnis','bogs_reparaturrueckstau'] },
  { nr: '9.3', label: 'Wert der Lasten und Beschränkungen', depth: 2, type: 'entwurf', editId: 'verfahren',
    aiTags: ['lasten_einleitung','lasten_beschreibung','lasten_urkunde','lasten_lageplan','lasten_beurteilung','lasten_einschaetzung','lasten_ergebnis'] },

  // 10. Verkehrswert
  { nr: '10', label: 'Verkehrswert', depth: 1, type: 'entwurf', editId: 'verfahren',
    aiTags: ['verkehrswert_ableitung','verkehrswert_feststellung','schlussvermerk'] },

  // 11. Literaturverzeichnis
  { nr: '11', label: 'Literaturverzeichnis', depth: 1, type: 'auto', hint: 'Wird automatisch zusammengestellt.' },

  // 12. Anlagen
  { nr: '12', label: 'Anlagen', depth: 1, type: 'heading' },
  { nr: null, label: 'Kopien aus den Bauplänen', depth: 2, type: 'anlagen_bauplaene' },
  { nr: null, label: 'Bildliche Darstellungen', depth: 2, type: 'anlagen_fotos' },
];

// TOC-Einträge: nur depth-1 Kapitel
const VORSCHAU_TOC = VORSCHAU_DOKUMENT.filter(b => b.depth === 1 && b.nr && !b.noHeading);

// ── Inline-editierbarer Absatz ──
const VorschauParagraph = ({ tagId, label, value, renderText, onSave, onRegenerate, status, isEdited, quellen, onOpenSource }) => {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(value || '');
  const [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState(false);
  const [hovered, setHovered] = useState(false);
  const textareaRef = useRef(null);
  const isGenerating = status === 'generating';

  // Herkunft (Transparenz): grobe Kategorie aus der Provenance-Map.
  // Das Badge ist klickbar und öffnet das Quell-Abgleich-Panel (Split-View).
  const prov = tagProvenanceFor(tagId);
  const provStyle = prov ? (PROVENANCE_STYLE[prov.kategorie] || PROVENANCE_STYLE.gemischt) : null;

  useEffect(() => { setDraft(value || ''); }, [value]);
  useEffect(() => {
    if (editing && textareaRef.current) {
      const ta = textareaRef.current;
      ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 'px';
      ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length);
    }
  }, [editing]);

  const handleSave = async () => {
    if (draft === value) { setEditing(false); return; }
    setSaving(true);
    try { await onSave(tagId, draft); setEditing(false); setSaved(true); setTimeout(() => setSaved(false), 1500); }
    catch (e) { console.error('[VorschauParagraph] Save:', e); }
    finally { setSaving(false); }
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Escape') { setDraft(value || ''); setEditing(false); }
    if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); handleSave(); }
  };

  if (editing) {
    return (
      <div style={{ margin: '0 0 10px 0' }}>
        <textarea ref={textareaRef} value={draft}
          onChange={e => { setDraft(e.target.value); e.target.style.height = 'auto'; e.target.style.height = e.target.scrollHeight + 'px'; }}
          onKeyDown={handleKeyDown} onBlur={() => handleSave()} disabled={saving}
          style={{ width: '100%', resize: 'none', overflow: 'hidden', fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif', fontSize: 14, lineHeight: 1.75, color: 'var(--text-primary)', padding: '8px 10px', border: '1.5px solid var(--vl-blue)', borderRadius: 4, background: '#FAFBFF', outline: 'none', boxSizing: 'border-box' }}
        />
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6, marginTop: 3, fontSize: 11, color: 'var(--text-tertiary)', fontFamily: 'var(--font-sans, sans-serif)' }}>
          <span>Esc = Abbrechen · Ctrl+Enter = Speichern</span>
        </div>
      </div>
    );
  }

  if (isGenerating) {
    return (
      <div style={{ margin: '0 0 10px 0', padding: 8, borderRadius: 4, background: 'var(--surface-light)', display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--text-tertiary)', fontFamily: 'var(--font-sans, sans-serif)' }}>
        <span className="entwurf-spinner" /> {label} wird generiert…
      </div>
    );
  }

  return (
    <p onClick={() => { setDraft(value || ''); setEditing(true); }}
      onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}
      title={`${label} — Klicken zum Bearbeiten`}
      style={{
        margin: '0 0 10px 0', fontSize: 14, lineHeight: 1.75,
        color: 'var(--text-primary)', textAlign: 'justify', hyphens: 'auto',
        cursor: 'text', padding: '4px 8px', marginLeft: -8, marginRight: -8,
        borderRadius: 4, position: 'relative',
        outline: hovered ? '1.5px solid var(--border-light)' : 'none',
        background: hovered ? '#FAFBFE' : 'none',
        transition: 'background 0.15s',
      }}
    >
      {renderText(value)}
      {hovered && prov && provStyle && (
        <button
          onClick={e => { e.stopPropagation(); if (!isEdited && onOpenSource) onOpenSource(tagId); }}
          title={isEdited ? 'Manuell überschrieben' : 'Quelle abgleichen'}
          style={{
            position: 'absolute', top: 4, left: 8,
            display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 10, fontWeight: 600,
            padding: '2px 8px', borderRadius: 99, border: 'none',
            cursor: isEdited ? 'default' : 'pointer',
            fontFamily: 'var(--font-sans, sans-serif)', letterSpacing: '0.02em',
            background: isEdited ? '#F4F6F8' : provStyle.bg,
            color: isEdited ? '#64748B' : provStyle.color,
          }}
        >
          {isEdited ? 'manuell' : provStyle.kurz}
          {!isEdited && (
            <span style={{ fontSize: 11, lineHeight: 1, opacity: 0.7 }}>›</span>
          )}
        </button>
      )}
      {onRegenerate && (
        <button onClick={e => { e.stopPropagation(); onRegenerate(tagId, !!isEdited); }}
          title="Neu generieren"
          style={{ position: 'absolute', top: 4, right: 4, width: 26, height: 26, borderRadius: '50%', background: 'var(--surface-light)', border: '1px solid var(--border-light)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, color: 'var(--text-tertiary)', fontFamily: 'var(--font-sans, sans-serif)', opacity: hovered ? 1 : 0, pointerEvents: hovered ? 'auto' : 'none', transition: 'opacity 0.15s' }}
          onMouseEnter={e => { e.currentTarget.style.background = 'var(--vl-blue)'; e.currentTarget.style.color = '#fff'; e.currentTarget.style.borderColor = 'var(--vl-blue)'; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'var(--surface-light)'; e.currentTarget.style.color = 'var(--text-tertiary)'; e.currentTarget.style.borderColor = 'var(--border-light)'; }}
        >{'\u21BB'}</button>
      )}
      {saved && (
        <span style={{ position: 'absolute', top: 2, right: 8, fontSize: 10, color: 'var(--success)', fontFamily: 'var(--font-sans, sans-serif)', fontWeight: 600, animation: 'entwurfSavedFade 1.5s ease forwards' }}>✓</span>
      )}
    </p>
  );
};

// ── Inline-Label Feld (Raumbeschreibungen) ──
// Rendert als "Fußböden: Text..." — wie im echten Gutachten
const VorschauInlineField = ({ tagId, label, value, status, renderText, onSave, onRegenerate }) => {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(value || '');
  const [saving, setSaving] = useState(false);
  const [hovered, setHovered] = useState(false);
  const textareaRef = useRef(null);

  useEffect(() => { if (!editing) setDraft(value || ''); }, [value, editing]);
  useEffect(() => {
    if (editing && textareaRef.current) {
      const ta = textareaRef.current;
      ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 'px'; ta.focus();
    }
  }, [editing]);

  const handleSave = async () => {
    if (draft === value) { setEditing(false); return; }
    setSaving(true);
    try { await onSave(tagId, draft); setEditing(false); }
    catch (e) { console.error('[VorschauInline] Save:', e); }
    finally { setSaving(false); }
  };

  if (editing) {
    return (
      <div style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'flex-start' }}>
        <span style={{ fontWeight: 700, fontSize: 14, minWidth: 120, flexShrink: 0, paddingTop: 9 }}>{label}:</span>
        <div style={{ flex: 1 }}>
          <textarea ref={textareaRef} value={draft}
            onChange={e => { setDraft(e.target.value); e.target.style.height = 'auto'; e.target.style.height = e.target.scrollHeight + 'px'; }}
            onKeyDown={e => { if (e.key === 'Escape') { setDraft(value||''); setEditing(false); } if (e.key === 'Enter' && (e.ctrlKey||e.metaKey)) { e.preventDefault(); handleSave(); } }}
            onBlur={() => handleSave()} disabled={saving}
            style={{ width: '100%', resize: 'none', overflow: 'hidden', fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif', fontSize: 14, lineHeight: 1.75, padding: '6px 8px', border: '1.5px solid var(--vl-blue)', borderRadius: 4, background: '#FAFBFF', outline: 'none', boxSizing: 'border-box' }}
          />
        </div>
      </div>
    );
  }

  return (
    <div
      onClick={() => { setDraft(value||''); setEditing(true); }}
      onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}
      style={{
        display: 'flex', gap: 8, marginBottom: 6, cursor: 'text',
        padding: '3px 8px', marginLeft: -8, marginRight: -8,
        borderRadius: 4, position: 'relative',
        outline: hovered ? '1.5px solid var(--border-light)' : 'none',
        background: hovered ? '#FAFBFE' : 'none',
        transition: 'background 0.15s',
      }}
    >
      <span style={{ fontWeight: 700, fontSize: 14, lineHeight: 1.75, minWidth: 120, flexShrink: 0 }}>{label}:</span>
      <span style={{ fontSize: 14, lineHeight: 1.75, color: value ? 'var(--text-primary)' : '#9CA3AF', fontStyle: value ? 'normal' : 'italic' }}>
        {value ? renderText(value) : 'noch offen'}
      </span>
      {onRegenerate && value && (
        <button onClick={e => { e.stopPropagation(); onRegenerate(tagId, status === 'edited'); }}
          title="Neu generieren"
          style={{ position: 'absolute', top: 2, right: 2, width: 26, height: 26, borderRadius: '50%', background: 'var(--surface-light)', border: '1px solid var(--border-light)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, color: 'var(--text-tertiary)', fontFamily: 'var(--font-sans, sans-serif)', opacity: hovered ? 1 : 0, pointerEvents: hovered ? 'auto' : 'none', transition: 'opacity 0.15s' }}
          onMouseEnter={e => { e.currentTarget.style.background = 'var(--vl-blue)'; e.currentTarget.style.color = '#fff'; e.currentTarget.style.borderColor = 'var(--vl-blue)'; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'var(--surface-light)'; e.currentTarget.style.color = 'var(--text-tertiary)'; e.currentTarget.style.borderColor = 'var(--border-light)'; }}
        >{'\u21BB'}</button>
      )}
    </div>
  );
};

// ── Hauptkomponente ──
// ── AbbildungSlot: Klickbarer Bild-Platzhalter mit Upload ──
const AbbildungSlot = ({ slot, storagePath, gutachtenId, projektId, session, workerUrl, onSave }) => {
  const [imgUrl, setImgUrl] = useState(null);
  const [uploading, setUploading] = useState(false);
  const [hovered, setHovered] = useState(false);
  const fileRef = useRef(null);

  // Signed URL laden wenn Bild vorhanden
  useEffect(() => {
    if (!storagePath || !session?.access_token || !workerUrl) { setImgUrl(null); return; }
    let cancelled = false;
    fetch(`${workerUrl}/api/signed-url?path=${encodeURIComponent(storagePath)}`, {
      headers: { Authorization: `Bearer ${session.access_token}` },
    })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (!cancelled && d?.url) setImgUrl(d.url); })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [storagePath, session, workerUrl]);

  const handleUpload = async (e) => {
    const file = e.target?.files?.[0];
    if (!file || !session?.access_token) return;
    setUploading(true);
    try {
      // Resize auf max 1200px Breite
      const blob = await new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
          const maxW = 1200;
          const scale = img.width > maxW ? maxW / img.width : 1;
          const c = document.createElement('canvas');
          c.width = img.width * scale; c.height = img.height * scale;
          c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
          c.toBlob(b => resolve(b), 'image/jpeg', 0.85);
        };
        img.src = URL.createObjectURL(file);
      });
      const fd = new FormData();
      fd.append('file', blob, `abb_${slot.id}.jpg`);
      fd.append('project_id', projektId);
      fd.append('gutachten_id', gutachtenId);
      const upRes = await fetch(`${workerUrl}/api/photo-upload`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${session.access_token}` },
        body: fd,
      });
      if (!upRes.ok) throw new Error('Upload fehlgeschlagen');
      const { storage_path } = await upRes.json();
      await onSave(storage_path);
    } catch (err) {
      console.error('[AbbildungSlot] Upload:', err);
      alert('Bild-Upload fehlgeschlagen: ' + err.message);
    } finally {
      setUploading(false);
      if (fileRef.current) fileRef.current.value = '';
    }
  };

  return (
    <div style={{ margin: '16px 0 20px', textAlign: 'center' }}>
      <input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={handleUpload} />
      <div
        onClick={() => !uploading && fileRef.current?.click()}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        style={{
          display: 'inline-flex', flexDirection: 'column', alignItems: 'center',
          padding: imgUrl ? 0 : '24px 32px',
          borderRadius: 8, minWidth: 260, maxWidth: '100%',
          border: `1.5px dashed ${hovered ? 'var(--vl-blue)' : '#CBD5E1'}`,
          background: hovered ? 'rgba(59,130,246,0.03)' : '#F8FAFC',
          cursor: uploading ? 'wait' : 'pointer',
          transition: 'border-color 0.15s, background 0.15s',
          position: 'relative', overflow: 'hidden',
        }}
      >
        {imgUrl ? (
          <>
            <img src={imgUrl} alt={slot.label}
              style={{ maxWidth: '100%', maxHeight: 400, borderRadius: 6, display: 'block' }} />
            {hovered && (
              <div style={{
                position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.4)',
                display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
                borderRadius: 6, color: '#fff', fontSize: 13, fontFamily: 'var(--font-sans, sans-serif)',
              }}>
                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
                </svg>
                <span style={{ marginTop: 6 }}>Bild ersetzen</span>
              </div>
            )}
          </>
        ) : uploading ? (
          <>
            <span className="entwurf-spinner" style={{ width: 24, height: 24 }} />
            <div style={{ fontSize: 12, color: 'var(--vl-blue)', marginTop: 8, fontFamily: 'var(--font-sans, sans-serif)' }}>
              Wird hochgeladen…
            </div>
          </>
        ) : (
          <>
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke={hovered ? 'var(--vl-blue)' : '#94A3B8'} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ transition: 'stroke 0.15s' }}>
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
            </svg>
            <div style={{ fontSize: 13, fontWeight: 600, color: hovered ? 'var(--vl-blue)' : 'var(--text-primary)', marginTop: 8, fontFamily: 'var(--font-sans, sans-serif)', transition: 'color 0.15s' }}>
              {slot.label}
            </div>
            <div style={{ fontSize: 11, color: '#94A3B8', marginTop: 2, fontFamily: 'var(--font-sans, sans-serif)' }}>
              {slot.desc}
            </div>
            <div style={{ fontSize: 11, color: hovered ? 'var(--vl-blue)' : '#CBD5E1', marginTop: 6, fontFamily: 'var(--font-sans, sans-serif)' }}>
              Klicken zum Hochladen
            </div>
          </>
        )}
      </div>
    </div>
  );
};


// ── Quell-Panel für den Abgleich (Transparenz Stufe 2) ──
// Zeigt rechts neben dem Entwurf die Quelle eines Bausteins zum eigenen Abgleich.
const QuellPanel = ({ panel, notes, notesLoaded, dokumente, workerUrl, session, onClose }) => {
  const [suche, setSuche] = useState('');
  const [openDoc, setOpenDoc] = useState(null); // { titel, file_path }
  const [docUrl, setDocUrl] = useState(null);
  const [docLoading, setDocLoading] = useState(false);

  const kat = String(panel?.kategorie || '');
  const zeigtOrtstermin = kat.includes('ortstermin');
  const zeigtDokument = kat.includes('dokument');
  const zeigtStammdaten = kat.includes('stammdaten');
  const nurWissen = kat === 'wissen';

  // PDF-URL holen, wenn ein Dokument geöffnet wird
  useEffect(() => {
    if (!openDoc?.file_path) { setDocUrl(null); return; }
    let active = true;
    setDocLoading(true);
    fetch(`${workerUrl}/api/signed-url?bucket=documents&path=${encodeURIComponent(openDoc.file_path)}`, {
      headers: { 'Authorization': `Bearer ${session?.access_token}` },
    })
      .then(r => r.json())
      .then(data => { const u = data?.url || data?.signedUrl; if (active && u) { setDocUrl(u); setDocLoading(false); } else if (active) { setDocLoading(false); } })
      .catch(() => { if (active) setDocLoading(false); });
    return () => { active = false; };
  }, [openDoc, workerUrl, session]);

  // Notizen filtern (Suche) + nach Kapitel gruppieren
  const gefilterteNotizen = (notes || [])
    .filter(n => n.type !== 'transcript_raw' && n.kapitel !== 'transcript_raw')
    .filter(n => n.text && n.text.trim())
    .filter(n => !suche.trim() || (n.text || '').toLowerCase().includes(suche.toLowerCase()));
  const notizenByKapitel = {};
  for (const n of gefilterteNotizen) {
    const k = n.kapitel || 'allgemein';
    (notizenByKapitel[k] = notizenByKapitel[k] || []).push(n);
  }

  // Vorhandene Dokumente (mit Datei)
  const docsVorhanden = (dokumente || []).filter(d => d.file_path);

  const headerFarbe = zeigtOrtstermin ? '#B45309' : zeigtDokument ? '#0D7A4F' : zeigtStammdaten ? '#0369A1' : '#6D28D9';

  return (
    <aside
      className="entwurf-quell-panel"
      style={{
        position: 'sticky', top: 80, flexShrink: 0, width: 420, maxHeight: 'calc(100vh - 100px)',
        display: 'flex', flexDirection: 'column', background: 'var(--surface, #fff)',
        border: '1px solid var(--border-light, #E5E7EB)', borderRadius: 12,
        boxShadow: '0 4px 20px rgba(0,0,0,0.06)', overflow: 'hidden',
        fontFamily: 'var(--font-sans, sans-serif)',
      }}
    >
      {/* Kopf */}
      <div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border-light)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
        <div>
          <div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-tertiary)', fontWeight: 700 }}>
            Quelle abgleichen
          </div>
          <div style={{ fontSize: 14, fontWeight: 700, color: headerFarbe, marginTop: 2 }}>{panel?.label || 'Quelle'}</div>
        </div>
        <button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 20, lineHeight: 1, color: 'var(--text-tertiary)', padding: 4 }} title="Schließen">×</button>
      </div>

      {/* Inhalt */}
      <div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>

        {/* KI-Wissen */}
        {nurWissen && (
          <div style={{ padding: 20, fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.6 }}>
            <div style={{ fontWeight: 600, color: '#6D28D9', marginBottom: 6 }}>KI-Fachwissen</div>
            Dieser Baustein wurde aus allgemeinem Fachwissen formuliert — es gibt keine
            konkrete Quelle aus Ortstermin oder Dokumenten zum Abgleich. Prüfe den Text
            inhaltlich auf fachliche Richtigkeit für dieses Objekt.
          </div>
        )}

        {/* Dokument-Ansicht */}
        {zeigtDokument && !openDoc && (
          <div style={{ padding: 12 }}>
            <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 10, lineHeight: 1.5 }}>
              Öffne ein Dokument, um zu prüfen, ob die Aussage dort belegt ist:
            </div>
            {docsVorhanden.length === 0 && (
              <div style={{ fontSize: 12, color: 'var(--text-tertiary)', fontStyle: 'italic' }}>Keine Dokumente mit Datei vorhanden.</div>
            )}
            {docsVorhanden.map((d, i) => (
              <button key={i} onClick={() => setOpenDoc({ titel: d.label || dokumenttypLabel(d.typ_raw), file_path: d.file_path })}
                style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 10px', marginBottom: 4, background: 'var(--surface-light, #F4F6F8)', border: '1px solid var(--border-light)', borderRadius: 7, cursor: 'pointer', fontSize: 12 }}>
                <span style={{ fontWeight: 600, color: '#0D7A4F' }}>{dokumenttypLabel(d.typ_raw)}</span>
                {d.label && d.label !== '(unbenannt)' ? <span style={{ color: 'var(--text-secondary)' }}> — {d.label}</span> : null}
              </button>
            ))}
          </div>
        )}
        {zeigtDokument && openDoc && (
          <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
            <div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border-light)', display: 'flex', alignItems: 'center', gap: 8 }}>
              <button onClick={() => setOpenDoc(null)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: 'var(--vl-blue)', fontWeight: 600 }}>‹ Zurück</button>
              <span style={{ fontSize: 12, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{openDoc.titel}</span>
            </div>
            <div style={{ flex: 1, minHeight: 400, background: '#fafaf8', position: 'relative' }}>
              {docLoading && <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>Lade Dokument…</div>}
              {docUrl && <iframe key={docUrl} src={docUrl} style={{ display: 'block', width: '100%', height: '100%', minHeight: 400, border: 'none' }} title={openDoc.titel} />}
            </div>
          </div>
        )}

        {/* Ortstermin-Notizen */}
        {zeigtOrtstermin && (
          <div style={{ padding: 12 }}>
            <input
              value={suche} onChange={e => setSuche(e.target.value)}
              placeholder="Notizen durchsuchen…"
              style={{ width: '100%', padding: '7px 10px', borderRadius: 7, border: '1px solid var(--border-light)', fontSize: 12, marginBottom: 10, boxSizing: 'border-box' }}
            />
            {!notesLoaded && <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Lade Notizen…</div>}
            {notesLoaded && gefilterteNotizen.length === 0 && (
              <div style={{ fontSize: 12, color: 'var(--text-tertiary)', fontStyle: 'italic' }}>
                {suche ? 'Keine Notizen für diese Suche.' : 'Keine Ortstermin-Notizen vorhanden.'}
              </div>
            )}
            {Object.entries(notizenByKapitel).map(([kap, items]) => (
              <div key={kap} style={{ marginBottom: 12 }}>
                <div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.04em', color: '#B45309', fontWeight: 700, marginBottom: 4 }}>
                  {NOTIZ_KAPITEL_LABELS[kap] || kap}
                </div>
                {items.map((n, i) => (
                  <div key={i} style={{ fontSize: 12.5, lineHeight: 1.55, color: 'var(--text-primary)', padding: '6px 8px', marginBottom: 3, background: 'var(--surface-light, #F4F6F8)', borderRadius: 6, whiteSpace: 'pre-wrap' }}>
                    {n.text}
                  </div>
                ))}
              </div>
            ))}
          </div>
        )}

        {/* Stammdaten-Hinweis (zusätzlich, wenn Stammdaten beteiligt) */}
        {zeigtStammdaten && !zeigtDokument && !zeigtOrtstermin && (
          <div style={{ padding: 20, fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.6 }}>
            <div style={{ fontWeight: 600, color: '#0369A1', marginBottom: 6 }}>Stammdaten des Gutachtens</div>
            Dieser Baustein stützt sich auf die erfassten Objekt-Stammdaten (inkl. der aus
            Dokumenten extrahierten Daten). Den Abgleich machst du am besten direkt in der
            Objekterfassung bzw. an den zugrunde liegenden Dokumenten.
          </div>
        )}
      </div>
    </aside>
  );
};

const EntwurfVorschau = ({ outputs, projekt, gutachten, session, workerUrl, onSave, onRegenerate }) => {
  const contentRef = useRef(null);
  const isMobile = useIsMobile();
  const [activeChapter, setActiveChapter] = useState('1');

  // ── Quell-Abgleich Split-View (Transparenz Stufe 2) ──
  // Klick auf ein Herkunfts-Badge öffnet rechts ein Panel mit der passenden Quelle:
  // Ortstermin → Notizen (mit Suche), Dokument → PDF-Viewer, KI → Hinweis.
  const [quellPanel, setQuellPanel] = useState(null); // { tagId, kategorie, label } | null
  const [panelNotes, setPanelNotes] = useState([]);
  const [panelNotesLoaded, setPanelNotesLoaded] = useState(false);

  // Notizen einmalig beim Mount laden — für die "Anwesende Personen"-Vorbefüllung
  // (Kap. 1) und für das Quell-Abgleich-Panel.
  useEffect(() => {
    if (panelNotesLoaded || !gutachten?.id) return;
    let active = true;
    ladeNotes(gutachten.id).then(rows => {
      if (active) { setPanelNotes(rows || []); setPanelNotesLoaded(true); }
    });
    return () => { active = false; };
  }, [gutachten?.id, panelNotesLoaded]);

  const openQuellPanel = (tagId) => {
    const prov = tagProvenanceFor(tagId);
    if (!prov) return;
    setQuellPanel({ tagId, kategorie: prov.kategorie, label: prov.label });
  };
  const closeQuellPanel = () => setQuellPanel(null);

  useEffect(() => {
    if (!contentRef.current) return;
    const headings = contentRef.current.querySelectorAll('[data-chapter-nr]');
    if (!headings.length) return;
    const observer = new IntersectionObserver(
      (entries) => { for (const e of entries) { if (e.isIntersecting) { setActiveChapter(e.target.dataset.chapterNr); break; } } },
      { rootMargin: '-80px 0px -60% 0px', threshold: 0 }
    );
    headings.forEach(h => observer.observe(h));
    return () => observer.disconnect();
  }, []);

  const scrollTo = (nr) => {
    const el = contentRef.current?.querySelector(`[data-chapter-nr="${nr}"]`);
    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  };

  // ── Dynamisches Dokument: Kap. 6 wird aus Bewertungsobjekten gebaut ──
  const objekte = gutachten?.objekte || [];
  const vorschauDoc = useMemo(() => {
    // Statische Blöcke vor Kap 6 (Kap 1-5)
    const idxKap6 = VORSCHAU_DOKUMENT.findIndex(b => b.nr === '6' && b.depth === 1);
    const idxKap7 = VORSCHAU_DOKUMENT.findIndex(b => b.nr === '7' && b.depth === 1);
    if (idxKap6 === -1 || idxKap7 === -1) return VORSCHAU_DOKUMENT;

    const before6 = VORSCHAU_DOKUMENT.slice(0, idxKap6);
    const after6 = VORSCHAU_DOKUMENT.slice(idxKap7);

    // Kap 6 Header
    const kap6Header = { nr: '6', label: 'Gebäude', depth: 1, type: 'heading', editId: 'gebaeude' };

    const hauptgebaeude = objekte.filter(o => o.kategorie === 'hauptgebaeude');
    const nebengebaeude = objekte.filter(o => o.kategorie === 'nebengebaeude');
    const kap6Blocks = [];
    let subNr = 1;

    // Hauptgebäude: volle Geschoss-Struktur
    for (const obj of hauptgebaeude) {
      const nr = `6.${subNr}`;
      const geschosse = new Set(obj.geschosse || ['kg', 'eg', 'dg']);

      kap6Blocks.push({ nr, label: obj.bezeichnung || 'Wohnhaus', depth: 2, type: 'entwurf', editId: 'gebaeude', objektId: obj.id,
        aiTags: ['wohnhaus_einleitung','bewertungseinheit_zuordnung','baujahre','gebaeudeart','hauseingang','vertikale_erschliessung'] });
      kap6Blocks.push({ nr: null, label: 'Konstruktiver Aufbau', depth: 3, type: 'entwurf', editId: 'gebaeude', objektId: obj.id, inlineLabels: true,
        aiTags: ['fassade','fenster_allgemein','dach','geschossdecken','heizungsanlage_allgemein'] });

      let gNr = 1;
      if (geschosse.has('kg')) {
        kap6Blocks.push({ nr: `${nr}.${gNr++}`, label: 'Kellergeschoss', depth: 3, type: 'entwurf', editId: 'gebaeude', objektId: obj.id, inlineLabels: true, introTag: 'kg_raumaufteilung',
          aiTags: ['kg_raumaufteilung','kg_fussboeden','kg_waende','kg_decken','kg_tueren','kg_fenster','kg_heizungsanlage','kg_beheizung','kg_sonstiges'] });
      }
      if (geschosse.has('eg')) {
        kap6Blocks.push({ nr: `${nr}.${gNr++}`, label: 'Erdgeschoss', depth: 3, type: 'entwurf', editId: 'gebaeude', objektId: obj.id, inlineLabels: true, introTag: 'eg_raumaufteilung',
          aiTags: ['eg_raumaufteilung','eg_fussboeden','eg_waende','eg_decken','eg_tueren','eg_fenster','eg_beheizung','eg_sanitaer','eg_sonstiges'] });
      }
      if (geschosse.has('dg')) {
        kap6Blocks.push({ nr: `${nr}.${gNr++}`, label: 'Dachgeschoss', depth: 3, type: 'entwurf', editId: 'gebaeude', objektId: obj.id, inlineLabels: true, introTag: 'dg_raumaufteilung',
          aiTags: ['dg_raumaufteilung','dg_fussboeden','dg_waende','dg_decken','dg_tueren','dg_fenster','dg_beheizung','dg_sanitaer','dg_sonstiges'] });
      }
      if (geschosse.has('sp')) {
        kap6Blocks.push({ nr: `${nr}.${gNr++}`, label: 'Spitzboden', depth: 3, type: 'entwurf', editId: 'gebaeude', objektId: obj.id, inlineLabels: true,
          aiTags: ['sp_zugang','sp_raumaufteilung','sp_fussboeden','sp_schraegen','sp_beheizung','sp_belichtung','sp_sonstiges'] });
      }
      subNr++;
    }

    // Nebengebäude: einfache Fließtext-Sections
    for (const obj of nebengebaeude) {
      kap6Blocks.push({ nr: `6.${subNr++}`, label: obj.bezeichnung || 'Garage', depth: 2, type: 'entwurf', editId: 'gebaeude', objektId: obj.id,
        aiTags: ['garage_einleitung', 'garage_ausstattung'] });
    }

    // Sonstige + Beurteilung (global, kein objektId)
    kap6Blocks.push({ nr: `6.${subNr}`, label: 'Sonstige bauliche Besonderheiten', depth: 2, type: 'entwurf', editId: 'gebaeude',
      aiTags: ['energieausweis', 'schornsteinfeger'] });
    kap6Blocks.push({ nr: null, label: 'Beurteilung', depth: 3, type: 'beurteilung', editId: 'gebaeude',
      aiTags: ['beurteilung_gebaeude_1','beurteilung_gebaeude_2','beurteilung_gebaeude_3','beurteilung_gebaeude_4'] });

    return [...before6, kap6Header, ...kap6Blocks, ...after6];
  }, [objekte]);

  // TOC aus dem dynamischen Dokument
  const vorschauToc = useMemo(() => vorschauDoc.filter(b => b.depth === 1 && b.nr && !b.noHeading), [vorschauDoc]);

  // [ERGÄNZEN: ...] Marker hervorheben
  const renderText = (text) => {
    if (!text) return null;
    const parts = text.split(/(\[ERGÄNZEN[^\]]*\])/gi);
    return parts.map((part, i) =>
      /^\[ERGÄNZEN/i.test(part)
        ? <mark key={i} style={{ background: '#FEF3C7', color: '#92400E', padding: '1px 4px', borderRadius: 3, fontWeight: 500, fontSize: '0.95em', border: '1px solid #FDE68A' }}>{part}</mark>
        : part
    );
  };

  // Stats für TOC
  const chapterStats = (nr) => {
    const blocks = vorschauDoc.filter(b => b.aiTags && (b.nr === nr || b.nr?.startsWith(nr + '.')));
    let total = 0, done = 0;
    blocks.forEach(b => {
      (b.aiTags || []).forEach(tid => {
        total++;
        const o = getOutput(outputs, tid, b.objektId);
        if (o && (o.status === 'generated' || o.status === 'edited')) done++;
      });
    });
    // Also count unnumbered blocks that follow this chapter
    const idx = vorschauDoc.findIndex(b => b.nr === nr && b.depth === 1);
    if (idx >= 0) {
      for (let i = idx + 1; i < vorschauDoc.length; i++) {
        const b = vorschauDoc[i];
        // Stop when we reach the NEXT numbered depth-1 chapter
        if (b.depth === 1 && b.nr && b.nr !== nr) break;
        // Count unnumbered entwurf blocks (Beurteilung, noHeading continuations)
        if (b.aiTags && !b.nr) {
          b.aiTags.forEach(tid => {
            total++;
            const o = getOutput(outputs, tid, b.objektId);
            if (o && (o.status === 'generated' || o.status === 'edited')) done++;
          });
        }
      }
    }
    return { total, done, pct: total === 0 ? 0 : Math.round((done / total) * 100) };
  };

  // Render a single document block
  const renderBlock = (block) => {
    const { nr, label, depth, type, aiTags, hint, editId, inlineLabels, subHeadings, introTag, objektId, demografieTabelle, grundstueckStammdaten, staticIntro } = block;

    // Heading sizes
    const headingStyle = depth === 1
      ? { fontSize: 20, fontWeight: 700, borderBottom: '1.5px solid var(--text-primary)', paddingBottom: 6, marginBottom: 20 }
      : depth === 2
        ? { fontSize: 16, fontWeight: 700, marginBottom: 12 }
        : { fontSize: 14, fontWeight: 600, marginBottom: 8, color: type === 'beurteilung' ? 'var(--vl-orange-dark)' : 'var(--text-primary)' };

    // Auto-generated placeholder
    if (type === 'auto') {
      // Kap. 1: Zusammenfassung — strukturierte Datentabelle
      if (nr === '1' && projekt) {
        const obj = gutachten?.objekte?.[0] || {};
        const gbDoc = projekt.dokumente?.find(d => d.typ_raw === 'grundbuchauszug' && d.extracted_fields?.fields);
        const gbFields = gbDoc?.extracted_fields?.fields || {};
        const gutachterName = projekt.sachverstaendiger || null;

        const auftraggeber = projekt.beteiligte?.find(b => b.rolle === 'auftraggeber');
        // Eigentümer: 1) Grundbuch-Extraktion (eigentuemer-Array), 2) Beteiligte mit Eigentümer-Rollen
        const eigentuemerFromGB = gbFields.eigentuemer && Array.isArray(gbFields.eigentuemer) && gbFields.eigentuemer.length > 0
          ? gbFields.eigentuemer.map(e => [e.name, e.anteil ? `(${e.anteil})` : null].filter(Boolean).join(' ')).join(', ')
          : null;
        const eigentuemerFromBeteiligte = projekt.beteiligte?.filter(b =>
          ['Antragsgegner', 'Antragsgegnerin', 'Schuldner', 'Schuldnerin', 'Eigentümer', 'Eigentümerin'].includes(b.rolle)
        ) || [];
        const eigentuemerStr = eigentuemerFromGB
          || (eigentuemerFromBeteiligte.length > 0 ? eigentuemerFromBeteiligte.map(e => e.name).join(', ') : null);

        // Stichtag: wertermittlungsstichtag, Fallback ortstermin_datum (bei ZV oft identisch)
        const stichtag = gutachten?.wertermittlungsstichtag || gutachten?.ortstermin_datum || null;

        // Objekttyp → menschenlesbare Art
        // Robust gegen Label/Roh-Wert: normObjekttyp normalisiert auf lowercase-Keys
        // (sonst greift artMap['Eigentumswohnung'] nicht).
        const ot = normObjekttyp(obj.objekttyp);
        const artMap = { efh: 'Wohngrundstück', etw: 'Wohngrundstück', mfh: 'Wohngrundstück (Mehrfamilienhaus)', dhh: 'Wohngrundstück (Doppelhaushälfte)' };
        const bebauungMap = { efh: 'Einfamilienhaus', etw: 'Eigentumswohnung', mfh: 'Mehrfamilienhaus', dhh: 'Doppelhaushälfte/Reihenhaus' };

        // Ortstermin: manuelles DB-Feld bevorzugt, sonst KI-Wert aus Notizen (ortstermin_ki).
        const ortsterminStr = gutachten?.ortstermin_datum
          ? formatDate(gutachten.ortstermin_datum) + (gutachten.ortstermin_uhrzeit ? `, Beginn ca. ${gutachten.ortstermin_uhrzeit.substring(0, 5)}` : '')
          : (outputs['ortstermin_ki']?.value || null);

        // Mieter-Info aus Entwurf-Tag oder Standard
        const mieterTag = outputs?.mietvertraege?.value;
        const mieterStr = mieterTag && mieterTag.trim() && mieterTag !== '' ? mieterTag : null;

        // Anwesende Personen: Vorrang hat das manuell gepflegte Gutachten-Feld;
        // fehlt es, werden die Sprachnotizen aus dem Ortstermin-Kapitel "anwesende"
        // als Vorbefüllung genutzt (roh, im Entwurf redigierbar → fließt via _kap1_anwesende
        // ins DOCX). Quelle: panelNotes (bereits für das Quell-Panel geladen).
        const anwesendeNotizen = (panelNotes || [])
          .filter(n => n.kapitel === 'anwesende' && n.text && n.text.trim())
          .map(n => n.text.trim());
        const ortAnwesende = gutachten?.ortstermin_anwesende
          || (outputs['ortstermin_anwesende_ki']?.value || null)
          || (anwesendeNotizen.length > 0 ? anwesendeNotizen.join('\n') : null);

        // Verfügbare Unterlagen — aus hochgeladenen Dokumenten + Standard
        // Label-Lookup aus UPLOAD_TYPES statt rohe Typ-IDs (z.B. "weg_verwalter" → "WEG-Verwalter-Kontakt")
        const docLabel = (typ) => {
          if (!typ) return null;
          for (const grp of UPLOAD_TYPES) {
            const found = grp.items?.find(i => i.id === typ);
            if (found) return found.label;
          }
          return typ; // Fallback: rohe ID
        };
        const uploadedDocs = (projekt.dokumente || [])
          .filter(d => d.file_path)
          .map(d => docLabel(d.typ) || d.label || d.typ);
        const standardUnterlagen = [
          'Bodenrichtwerte des Gutachterausschusses',
          'Grundstücksmarktbericht des Gutachterausschusses',
          'Auskünfte regionaler Marktteilnehmer',
          'eigene Kaufpreissammlung',
          'eigene Mietpreissammlung',
        ];
        const alleUnterlagen = [...new Set([...uploadedDocs, ...standardUnterlagen])];

        // Adresse "Straße, PLZ Ort" → "PLZ Ort, Straße" (konsistent mit DOCX-Export)
        const fmtAnschrift = (v) => {
          const s = (v || '').trim();
          if (!s) return v;
          const teile = s.split(',').map(t => t.trim()).filter(Boolean);
          if (teile.length !== 2) return s;
          const [strasse, ortTeil] = teile;
          if (/^\d{5}\b/.test(ortTeil) || /^[A-ZÄÖÜ]/.test(ortTeil)) return `${ortTeil}, ${strasse}`;
          return s;
        };

        const rows = [
          ['Auftraggeber', auftraggeber ? [auftraggeber.name, auftraggeber.anschrift].filter(Boolean).join('\n') : (projekt.auftraggeber || null), '_kap1_auftraggeber'],
          ['Verantwortlicher Gutachter', gutachterName, '_kap1_gutachter'],
          ['Auftragsbeschreibung', projekt.auftragsbeschreibung || null, '_kap1_auftragsbeschreibung'],
          ['Objektanschrift', fmtAnschrift(gutachten?.adresse), '_kap1_adresse'],
          ['Bezeichnung und Größe\ndes Grundstücks', [
            gbFields.gemarkung ? `Gemarkung ${gbFields.gemarkung}` : null,
            gbFields.flurstueck ? `Flurstück Nr. ${gbFields.flurstueck}` : null,
            gbFields.groesse_qm ? `${gbFields.groesse_qm} m²` : (obj.groesse || null),
          ].filter(Boolean).join(', ') || null, '_kap1_grundstueck'],
          ['Art', artMap[ot] || obj.objekttyp || null, '_kap1_art'],
          ['Vorhandene Bebauung', gutachten?.vorhandene_bebauung_beschreibung || (ot !== 'etw' ? (outputs['bewertungseinheit_kurz']?.value || null) : null) || bebauungMap[ot] || obj.bezeichnung || null, '_kap1_bebauung', ot !== 'etw' ? 'bewertungseinheit_kurz' : null],
          ['Baujahr ca.', obj.baujahr || null, '_kap1_baujahr'],
          ['Wohnfläche ca.', obj.wohnflaeche ? `${obj.wohnflaeche} m²` : null, '_kap1_wohnflaeche'],
          // ETW (Gruppe C): "Art und Lage des Bewertungsobjekts" + "Miteigentumsanteil
          // am Grundstück". Bei mehreren Einheiten (z.B. Wohnung + Tiefgaragen-
          // stellplätze) durchnummeriert (I, II, III …), bei genau einer Einheit
          // ohne Ziffer — wie in den Mustergutachten.
          ...(ot === 'etw' ? (() => {
            // Bei einem ETW-Gutachten gehören ALLE Bewertungsobjekte zur
            // Bewertungseinheit (Wohnung + Tiefgaragenstellplätze + Keller etc.),
            // auch wenn einzelne als 'stellplatz' typisiert sind. Sortiert nach
            // sort_order; die Hauptwohnung (kategorie 'hauptgebaeude') zuerst.
            const alle = (gutachten?.objekte || []);
            const liste = alle.length > 0 ? alle : [obj];
            const roman = (n) => ['I','II','III','IV','V','VI','VII','VIII','IX','X'][n] || String(n + 1);
            const mehrere = liste.length > 1;
            return liste.flatMap((o, idx) => {
              const suffix = mehrere ? ` ${roman(idx)}` : '';
              // KI-Vorschlag (Punkt 14) nur fürs Hauptobjekt als Fallback, wenn
              // manueller Wert leer. Bei Mehrobjekt pflegt der Gutachter II/III manuell.
              const kiVorschlag = idx === 0 ? (outputs['bewertungseinheit_kurz']?.value || null) : null;
              return [
                [`Art und Lage des\nBewertungsobjekts${suffix}`, o.art_lage_bewertungsobjekt || kiVorschlag || null, `_kap1_art_lage_${idx}`, idx === 0 ? 'bewertungseinheit_kurz' : null],
                ['Miteigentumsanteil am\nGrundstück', o.mea || null, `_kap1_miteigentumsanteil_${idx}`],
              ];
            });
          })() : []),
          ['Eigentümer lt. Grundbuch', eigentuemerStr, '_kap1_eigentuemer'],
          ['Mieter', mieterStr || 'Keiner.', '_kap1_mieter'],
          ['Ortstermin', ortsterminStr, '_kap1_ortstermin'],
          ['Beim Ortstermin\nanwesende Personen', ortAnwesende, '_kap1_anwesende'],
          ['Umfang der Besichtigung', 'Das Bewertungsobjekt konnte vollständig, sowohl von innen als auch von außen, besichtigt werden. Die Besichtigung ist beschränkt auf die zugänglichen und sichtbaren Teile des Gebäudes; verdeckte Mängel können nicht ausgeschlossen werden!', '_kap1_besichtigung'],
          ['Wertermittlungs-/\nQualitätsstichtag', (() => {
            if (stichtag) return `${formatDate(stichtag)} (Tag der Ortsbegehung)`;
            // Kein DB-Datum → Datum aus dem KI-Ortstermin-Text ableiten (TT.MM.JJJJ)
            const kiOt = outputs['ortstermin_ki']?.value;
            const m = kiOt && String(kiOt).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
            if (m) return `${m[1].padStart(2,'0')}.${m[2].padStart(2,'0')}.${m[3]} (Tag der Ortsbegehung)`;
            return null;
          })(), '_kap1_stichtag'],
          ['Verfügbare Unterlagen bzw.\nInformationen', alleUnterlagen.length > 0 ? alleUnterlagen.map(u => `–  ${u}`).join('\n') : null, '_kap1_unterlagen'],
          ['Wertbestimmendes Verfahren', gutachten?.wertermittlungsmethode || 'Sachwertverfahren', '_kap1_verfahren'],
          ['Bodenwert', gutachten?.bodenwert ? `${Number(gutachten.bodenwert).toLocaleString('de-DE')} €` : null, '_kap1_bodenwert'],
          ['Sachwert', gutachten?.sachwert ? `${Number(gutachten.sachwert).toLocaleString('de-DE')} €` : null, '_kap1_sachwert'],
          ['Ausfertigungsdatum und\nAbschluss der Recherchen', gutachten?.ausfertigungsdatum ? formatDate(gutachten.ausfertigungsdatum) : null, '_kap1_ausfertigungsdatum'],
        ];

        return (
          <div key={nr} style={{ marginBottom: 36 }}>
            <div data-chapter-nr={nr} style={{ ...headingStyle }}>
              <span style={{ fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>{nr}. {label}</span>
            </div>
            <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13, fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>
              <tbody>
                {rows.map(([lbl, computedVal, overrideId, kiTagId], i) => {
                  const override = outputs[overrideId]?.value;
                  const displayVal = override || computedVal;
                  return (
                    <tr key={i} style={{ borderBottom: '1px solid #E5E7EB' }}>
                      <td style={{ padding: '8px 12px 8px 0', verticalAlign: 'top', width: '40%', color: 'var(--text-secondary)', whiteSpace: 'pre-line' }}>{lbl}:</td>
                      <td style={{ padding: '0', verticalAlign: 'top' }}>
                        <VorschauParagraph
                          tagId={overrideId}
                          label={lbl}
                          value={displayVal || ''}
                          status={outputs[overrideId]?.status || (kiTagId ? outputs[kiTagId]?.status : undefined)}
                          isEdited={outputs[overrideId]?.is_manually_edited}
                          renderText={t => <span style={{ whiteSpace: 'pre-line', color: t ? 'var(--text-primary)' : '#D1D5DB', fontStyle: t ? 'normal' : 'italic' }}>{t || '[ERGÄNZEN]'}</span>}
                          onSave={onSave}
                          onRegenerate={kiTagId ? (() => onRegenerate(kiTagId, false)) : undefined}
                        />
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
            <VorschauParagraph
              tagId="_kap1_haftungshinweis"
              label="Haftungshinweis"
              value={outputs['_kap1_haftungshinweis']?.value || 'Dieses Verkehrswertgutachten ist kein Bausubstanzgutachten. Es wurden nur augenscheinliche, stichprobenartige Feststellungen getroffen, vorhandene Abdeckungen von Wand-, Boden- oder Deckenflächen wurden nicht entfernt. Bei der Substanzbeschreibung wird eine übliche Ausführungsart und ggf. die Richtigkeit von Angaben unterstellt. Weiterhin wird vorausgesetzt, dass – bis auf evtl. festgestellte Mängel – die zum Bauzeitpunkt gültigen einschlägigen technischen Vorschriften und Normen (z.B. Statik, Schall- und Wärmeschutz, Brandschutz etc.) eingehalten worden sind. Die formelle und materielle Legalität der vorhandenen baulichen Anlagen und Außenanlagen wird vorausgesetzt. Unterlagen, die über die im Gutachten genannten hinausgehen, wurden vom Sachverständigen nicht weiter überprüft, evtl. Inhalte wurden somit bei der Bewertung nicht berücksichtigt.'}
              status={outputs['_kap1_haftungshinweis']?.status}
              renderText={t => <span style={{ fontSize: 12, color: '#6B7280', fontStyle: 'italic', lineHeight: 1.7 }}>{t}</span>}
              onSave={onSave}
            />
          </div>
        );
      }

      // Kap. 2: Grundbuchdaten — aus allen Grundbuch-Extraktionen
      if (nr === '2' && projekt) {
        const gbDocs = (projekt.dokumente || [])
          .filter(d => d.typ_raw === 'grundbuchauszug' && d.extracted_fields?.fields)
          .map(d => d.extracted_fields.fields);
        const hasData = gbDocs.length > 0;

        // Amtsgericht: erst aus Extraktion, dann aus Auftraggeber ableiten
        const deriveAmtsgericht = (gb) => {
          if (gb.grundbuchamt) return gb.grundbuchamt;
          const ag = projekt.auftraggeber || '';
          const m = ag.match(/Amtsgericht\s+(\S+)/i);
          return m ? m[1].replace(/,$/, '') : '[ERGÄNZEN]';
        };

        const tblStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 12, marginBottom: 20 };
        const thStyle = { textAlign: 'left', padding: '4px 8px', borderBottom: '2px solid #374151', fontSize: 11, color: 'var(--text-secondary)' };
        const tdStyle = { padding: '6px 8px', borderBottom: '1px solid #E5E7EB', verticalAlign: 'top' };

        return (
          <div key={nr} style={{ marginBottom: 36 }}>
            <div data-chapter-nr={nr} style={{ ...headingStyle }}>
              <span style={{ fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>{nr}. {label}</span>
            </div>
            {hasData ? gbDocs.map((gb, gbIdx) => {
              const bv = (gb.bestandsverzeichnis || []).filter(e => !e.gestrichen);
              const bvAll = gb.bestandsverzeichnis || [];
              const abt2 = gb.abteilung_ii || [];
              const abt1 = (gb.abteilung_i || []).filter(e => !e.gestrichen);
              const amtsgericht = deriveAmtsgericht(gb);

              return (
                <div key={gbIdx} style={{ fontSize: 13, fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif', lineHeight: 1.75, marginBottom: gbDocs.length > 1 ? 32 : 0 }}>
                  {/* Einleitungssatz */}
                  <VorschauParagraph
                    tagId={`_kap2_einleitung${gbIdx > 0 ? '_' + gbIdx : ''}`}
                    label="Grundbuch-Einleitung"
                    value={outputs[`_kap2_einleitung${gbIdx > 0 ? '_' + gbIdx : ''}`]?.value || `Im Grundbuch von ${gb.gemarkung || '[ERGÄNZEN]'} des Amtsgerichts ${amtsgericht}, Blatt ${gb.grundbuchblatt || '[ERGÄNZEN]'}, Ausdruck vom ${gb.auszug_datum ? formatDate(gb.auszug_datum) : '[ERGÄNZEN]'}, sind folgende Eintragungen dargestellt:`}
                    status={outputs[`_kap2_einleitung${gbIdx > 0 ? '_' + gbIdx : ''}`]?.status}
                    renderText={t => t}
                    onSave={onSave}
                  />

                  {/* Bestandsverzeichnis */}
                  <div style={{ fontWeight: 700, marginBottom: 8 }}>Bestandsverzeichnis</div>
                  <table style={tblStyle}>
                    <thead>
                      <tr>
                        <th style={{ ...thStyle, width: '8%' }}>Lfd. Nr.</th>
                        <th style={{ ...thStyle, width: '8%' }}>Bish.</th>
                        <th style={{ ...thStyle, width: '15%' }}>Flurstück</th>
                        <th style={thStyle}>Wirtschaftsart und Lage</th>
                        <th style={{ ...thStyle, textAlign: 'right', width: '12%' }}>Größe m²</th>
                      </tr>
                    </thead>
                    <tbody>
                      {bvAll.length > 0 ? bvAll.map((e, i) => (
                        <tr key={i} style={e.gestrichen ? { textDecoration: 'line-through', color: '#9CA3AF' } : {}}>
                          <td style={tdStyle}>{e.lfd_nr}</td>
                          <td style={tdStyle}>{e.bisherige_lfd_nr || ''}</td>
                          <td style={tdStyle}>{e.flurstueck}</td>
                          <td style={tdStyle}>{e.gestrichen ? 'Gestrichen' : e.wirtschaftsart}</td>
                          <td style={{ ...tdStyle, textAlign: 'right' }}>{e.gestrichen ? '' : e.groesse_qm}</td>
                        </tr>
                      )) : (
                        <tr>
                          <td style={tdStyle}>2</td>
                          <td style={tdStyle}></td>
                          <td style={tdStyle}>{gb.flurstueck || '[ERGÄNZEN]'}</td>
                          <td style={tdStyle}>{gb.wirtschaftsart || '[ERGÄNZEN]'}</td>
                          <td style={{ ...tdStyle, textAlign: 'right' }}>{gb.groesse_qm || '[ERGÄNZEN]'}</td>
                        </tr>
                      )}
                    </tbody>
                  </table>

                  {/* Abteilung II */}
                  {abt2.length > 0 && (
                    <>
                      <div style={{ fontWeight: 700, marginBottom: 8 }}>Abteilung II</div>
                      <table style={tblStyle}>
                        <thead>
                          <tr>
                            <th style={{ ...thStyle, width: '8%' }}>Lfd. Nr.</th>
                            <th style={{ ...thStyle, width: '8%' }}>Lfd. Nr. BV</th>
                            <th style={thStyle}>Lasten und Beschränkungen</th>
                          </tr>
                        </thead>
                        <tbody>
                          {abt2.map((e, i) => (
                            <tr key={i}>
                              <td style={tdStyle}>{e.lfd_nr}</td>
                              <td style={tdStyle}>{e.betroffene_grundstuecke}</td>
                              <td style={{ ...tdStyle, lineHeight: 1.6 }}>{e.text}</td>
                            </tr>
                          ))}
                        </tbody>
                      </table>
                    </>
                  )}
                  {abt2.length === 0 && gb.abt_2_eintragungen && gb.abt_2_eintragungen !== 'keine' && (
                    <>
                      <div style={{ fontWeight: 700, marginBottom: 8 }}>Abteilung II</div>
                      <VorschauParagraph
                        tagId={`_kap2_abt2${gbIdx > 0 ? '_' + gbIdx : ''}`}
                        label="Abteilung II"
                        value={outputs[`_kap2_abt2${gbIdx > 0 ? '_' + gbIdx : ''}`]?.value || gb.abt_2_eintragungen}
                        status={outputs[`_kap2_abt2${gbIdx > 0 ? '_' + gbIdx : ''}`]?.status}
                        renderText={t => t}
                        onSave={onSave}
                      />
                    </>
                  )}

                  {/* Schlusshinweis */}
                  <VorschauParagraph
                    tagId={`_kap2_schluss${gbIdx > 0 ? '_' + gbIdx : ''}`}
                    label="Grundbuch-Schluss"
                    value={outputs[`_kap2_schluss${gbIdx > 0 ? '_' + gbIdx : ''}`]?.value || 'Auf die Darstellung von Eintragungen in Abteilung I und III wird verzichtet, da diese für die Wertermittlung nicht relevant sind.'}
                    status={outputs[`_kap2_schluss${gbIdx > 0 ? '_' + gbIdx : ''}`]?.status}
                    renderText={t => <span style={{ fontSize: 12, fontStyle: 'italic', color: '#6B7280' }}>{t}</span>}
                    onSave={onSave}
                  />
                </div>
              );
            }) : (
              <div style={{
                padding: '14px 18px', borderRadius: 6,
                background: '#F9FAFB', border: '1px dashed #D1D5DB',
                color: '#6B7280', fontSize: 13, fontStyle: 'italic',
                fontFamily: 'var(--font-sans, sans-serif)',
              }}>
                Grundbuchauszug noch nicht hochgeladen. Nach dem Upload werden die Grundbuchdaten hier automatisch dargestellt.
              </div>
            )}
          </div>
        );
      }

      // Andere Auto-Kapitel (7, 11, 12): weiterhin Platzhalter
      return (
        <div key={nr} style={{ marginBottom: depth === 1 ? 36 : 20 }}>
          <div data-chapter-nr={nr} style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', ...headingStyle }}>
            <span style={{ fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>{nr}. {label}</span>
          </div>
          <div style={{
            padding: '14px 18px', borderRadius: 6,
            background: '#F9FAFB', border: '1px dashed #D1D5DB',
            color: '#6B7280', fontSize: 13, fontStyle: 'italic',
            fontFamily: 'var(--font-sans, sans-serif)',
          }}>
            {hint}
          </div>
        </div>
      );
    }

    // Pure heading (no own content, just a chapter opener)
    if (type === 'heading') {
      return (
        <div key={nr} data-chapter-nr={nr} style={{ ...headingStyle, display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: depth === 1 ? 24 : 14, marginTop: depth === 1 ? 48 : 0 }}>
          <span style={{ fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>{nr}. {label}</span>
        </div>
      );
    }

    // Statischer Text (z.B. generischer Einleitungstext Kap. 4)
    if (type === 'static') {
      return (
        <div key={`static-${label || 'text'}`} style={{ marginBottom: 20 }}>
          <p style={{
            margin: 0, fontSize: 14, lineHeight: 1.7, textAlign: 'justify',
            fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif',
            fontStyle: block.italic ? 'italic' : 'normal',
          }}>
            {block.text}
          </p>
        </div>
      );
    }

    // Abbildung — Bild-Platzhalter oder hochgeladenes Bild
    if (type === 'abbildung') {
      const slot = GUTACHTEN_ABBILDUNGEN.find(a => a.id === block.slotId);
      if (!slot) return null;
      const overrideKey = `_abb_${slot.id}`;
      // Mapping: GUTACHTEN_ABBILDUNGEN id → foto_zuordnung key
      const ABBILD_TO_FOTO = {
        titelbild: 'foto_titelbild', lagekarte: 'foto_lagekarte',
        mikrolage_karte: 'foto_mikrolage', lageplan: 'foto_lageplan',
        luftbild: 'foto_luftbild', bebauungsplan: 'foto_bebauungsplan',
        laerm_lden: 'foto_laerm_lden', laerm_lnight: 'foto_laerm_lnight',
      };
      const fotoKey = ABBILD_TO_FOTO[slot.id] || `foto_${slot.id}`;
      const storagePath = outputs[overrideKey]?.value || gutachten?.foto_zuordnung?.[fotoKey] || null;

      return (
        <AbbildungSlot
          key={`abb-${slot.id}`}
          slot={slot}
          storagePath={storagePath}
          gutachtenId={gutachten?.id}
          projektId={projekt?.id}
          session={session}
          workerUrl={workerUrl}
          onSave={(path) => onSave(overrideKey, path)}
        />
      );
    }

    // Anlagen: Baupläne — Grid mit Bauplan-Slots
    if (type === 'anlagen_bauplaene') {
      const bauplanSlots = GUTACHTEN_ABBILDUNGEN.filter(a => a.category === 'bauplan');
      return (
        <div key="anlagen-bauplaene" style={{ marginBottom: 32 }}>
          <div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12, textDecoration: 'underline', fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>
            {label}
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 10 }}>
            {bauplanSlots.map(slot => (
              <div key={slot.id} style={{
                display: 'flex', flexDirection: 'column', alignItems: 'center',
                padding: '16px 12px', borderRadius: 8,
                border: '1.5px dashed #CBD5E1', background: '#F8FAFC', textAlign: 'center',
              }}>
                <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#94A3B8" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>
                </svg>
                <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)', marginTop: 6, fontFamily: 'var(--font-sans, sans-serif)' }}>{slot.label}</div>
                <div style={{ fontSize: 10, color: '#94A3B8', fontFamily: 'var(--font-sans, sans-serif)' }}>{slot.desc}</div>
              </div>
            ))}
          </div>
        </div>
      );
    }

    // Anlagen: Fotodokumentation — Kategorien
    if (type === 'anlagen_fotos') {
      return (
        <div key="anlagen-fotos" style={{ marginBottom: 32 }}>
          <div style={{ fontSize: 14, fontWeight: 700, marginBottom: 16, textDecoration: 'underline', fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>
            {label}
          </div>
          {FOTO_KATEGORIEN.map(cat => (
            <div key={cat.id} style={{ marginBottom: 16 }}>
              <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 8, fontFamily: 'var(--font-sans, sans-serif)' }}>
                {cat.label}
              </div>
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                {[1, 2].map(i => (
                  <div key={i} style={{
                    width: 100, height: 75, borderRadius: 6,
                    border: '1.5px dashed #CBD5E1', background: '#F8FAFC',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                  }}>
                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#CBD5E1" strokeWidth="1.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>
      );
    }

    // Entwurf content block or Beurteilung
    const tags = aiTags || [];
    const hasAnyContent = grundstueckStammdaten || tags.some(tid => getOutput(outputs, tid, objektId)?.value);

    return (
      <div key={`${nr||'b'}-${label||'cont'}-${objektId||'g'}`} style={{ marginBottom: block.noHeading ? 0 : (depth === 1 ? 36 : depth === 2 ? 24 : 16), marginTop: block.noHeading ? 0 : (depth === 1 ? 48 : 0) }}>
        {/* Section heading (suppressed when noHeading) */}
        {!block.noHeading && (
        <div
          {...(depth <= 2 && nr ? { 'data-chapter-nr': nr } : {})}
          style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', ...headingStyle }}
        >
          <span style={{ fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>
            {type === 'beurteilung' ? '' : nr ? `${nr} ` : ''}{label}{type === 'beurteilung' ? ':' : ''}
          </span>
        </div>
        )}

        {/* Kursiver Standardtext (z.B. Kap 4.3 Umwelt-Disclaimer) */}
        {staticIntro && (
          <p style={{
            margin: '0 0 16px', fontSize: 14, lineHeight: 1.7, textAlign: 'justify',
            fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif',
            fontStyle: 'italic',
          }}>
            {staticIntro}
          </p>
        )}

        {/* Content */}
        {hasAnyContent ? (
          <div>
            {/* Grundstück 4.1: Kombinierte Stammdaten + AI-Tags in PDF-Reihenfolge */}
            {grundstueckStammdaten && (() => {
              const obj0 = gutachten?.objekte?.[0] || {};
              const gbDoc = projekt?.dokumente?.find(d => d.typ_raw === 'grundbuchauszug' && d.extracted_fields?.fields);
              const gbGroesseRaw = gbDoc?.extracted_fields?.fields?.groesse_qm;
              const objGroesse = obj0.groesse; // already "796 m²" formatted
              const groesseStr = gbGroesseRaw
                ? `${Number(gbGroesseRaw).toLocaleString('de-DE')} m²`
                : objGroesse || null;
              // Reihenfolge exakt wie im V&L-Gutachten
              const rows = [
                { id: '_kap4_grundstuecksgroesse', label: 'Grundstücksgröße gemäß\nGrundbuch:', defaultVal: groesseStr, isStamm: true },
                { id: 'grundstueckszuschnitt', label: entwurfTagLabel('grundstueckszuschnitt'), isAI: true },
                { id: '_kap4_strassenfront', label: 'Breite an der Straßenfront:', isStamm: true },
                { id: '_kap4_mittlere_breite', label: 'Mittlere Breite ca.:', isStamm: true },
                { id: '_kap4_mittlere_tiefe', label: 'Mittlere Tiefe ca.:', isStamm: true },
                { id: 'topographie', label: entwurfTagLabel('topographie'), isAI: true },
                { id: 'hoehenlage', label: entwurfTagLabel('hoehenlage'), isAI: true },
                { id: 'ausrichtung', label: entwurfTagLabel('ausrichtung'), isAI: true },
              ];
              return rows.map(r => {
                const out = getOutput(outputs, r.id, objektId);
                const val = out?.value || r.defaultVal || null;
                return (
                  <VorschauInlineField
                    key={r.id}
                    tagId={r.id}
                    label={r.label}
                    value={val}
                    status={out?.status || (r.defaultVal ? 'generated' : null)}
                    renderText={renderText}
                    onSave={(t, v) => onSave(t, v, objektId)}
                    onRegenerate={r.isAI ? ((t, e) => onRegenerate(t, e, objektId)) : (() => {})}
                  />
                );
              });
            })()}
            {/* Reguläre Tags (bei grundstueckStammdaten schon inline gerendert → skip) */}
            {!grundstueckStammdaten && tags.map((tid, tidx) => {
              const out = getOutput(outputs, tid, objektId);
              const val = out?.value || null;
              const tagLabel = entwurfTagLabel(tid);
              const isIntro = introTag && tid === introTag;

              // WEG/TE-only Tags: nur anzeigen wenn Inhalt generiert wurde
              const WEG_ONLY_TAGS = ['einleitung_teileigentumsgrundbuch', 'gemeinschaftseigentum', 'nachtraege_teilungserklaerung', 'einleitung_wohnungsgrundbuch'];
              if (WEG_ONLY_TAGS.includes(tid) && !val) return null;

              // "Ausstattung" Zwischenüberschrift: nach dem Intro-Absatz, vor den gelabelten Items
              const showAusstattungHeading = introTag && inlineLabels && tidx > 0
                && tags[tidx - 1] === introTag;

              return (
                <Fragment key={`${tid}-${objektId||'g'}`}>
                  {showAusstattungHeading && (
                    <div style={{
                      fontSize: 14, fontWeight: 700, marginTop: 16, marginBottom: 8,
                      fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif',
                    }}>
                      Ausstattung
                    </div>
                  )}
                  {subHeadings && (() => {
                    // Gruppierung: Überschrift nur zeigen wenn Label sich ändert (z.B. "Freiflächengestaltung" nur einmal)
                    const prevLabel = tidx > 0 ? entwurfTagLabel(tags[tidx - 1]) : null;
                    if (tagLabel === prevLabel) return null;
                    return (
                      <div style={{
                        fontSize: 14, fontWeight: 700, marginTop: tidx > 0 ? 18 : 0, marginBottom: 8,
                        fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif',
                        textDecoration: 'underline',
                      }}>
                        {tagLabel}
                      </div>
                    );
                  })()}
                  {/* B-Plan Festsetzungen-Tabelle direkt nach der Bauplanungsrecht-Überschrift */}
                  {tid === 'bauplanungsrecht' && (() => {
                    const obj0 = gutachten?.objekte?.[0] || {};
                    const hasData = obj0.art_bauliche_nutzung || obj0.grz || obj0.bauweise;
                    if (!hasData) return null;
                    const tdS = { padding: '4px 12px 4px 0', verticalAlign: 'top', fontSize: 14, fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' };
                    const thS = { ...tdS, fontWeight: 700, whiteSpace: 'nowrap', paddingRight: 24, verticalAlign: 'top' };
                    // Nutzungsart expandieren (Kürzel → Langform)
                    const NUTZUNG_MAP = {
                      'WA': 'Allgemeines Wohngebiet (WA)', 'WR': 'Reines Wohngebiet (WR)',
                      'MI': 'Mischgebiet (MI)', 'MU': 'Urbanes Gebiet (MU)',
                      'GE': 'Gewerbegebiet (GE)', 'GI': 'Industriegebiet (GI)',
                      'MK': 'Kerngebiet (MK)', 'MD': 'Dorfgebiet (MD)',
                      'SO': 'Sondergebiet (SO)', 'WB': 'Besonderes Wohngebiet (WB)',
                    };
                    const nutzungStr = NUTZUNG_MAP[obj0.art_bauliche_nutzung] || obj0.art_bauliche_nutzung;
                    const massStr = [
                      obj0.grz ? `GRZ ${obj0.grz}` : null,
                      obj0.gfz ? `GFZ ${obj0.gfz}` : null,
                      obj0.vollgeschosse_zulaessig ? `${obj0.vollgeschosse_zulaessig} Vollgeschosse` : null,
                    ].filter(Boolean).join(', ');
                    const bauweiseStr = [
                      obj0.bauweise,
                      obj0.dachform,
                    ].filter(Boolean).join(', ');
                    return (
                      <>
                        <table style={{ width: '100%', borderCollapse: 'collapse', margin: '8px 0 12px' }}>
                          <tbody>
                            {nutzungStr && <tr><td style={thS}>Art der baulichen Nutzung:</td><td style={tdS}>{nutzungStr}</td></tr>}
                            {massStr && <tr><td style={thS}>Maß der baulichen Nutzung:</td><td style={tdS}>{massStr}</td></tr>}
                            {bauweiseStr && <tr><td style={thS}>Bauweise:</td><td style={tdS}>{bauweiseStr}</td></tr>}
                          </tbody>
                        </table>
                      </>
                    );
                  })()}
                  {inlineLabels && !isIntro
                    ? <VorschauInlineField tagId={tid} label={tagLabel} value={val} status={out?.status} renderText={renderText} onSave={(t, v) => onSave(t, v, objektId)} onRegenerate={(t, e) => onRegenerate(t, e, objektId)} />
                    : !val
                      ? <p style={{ margin: '0 0 6px 0', color: '#9CA3AF', fontStyle: 'italic', fontSize: 13, fontFamily: 'var(--font-sans, sans-serif)' }}>[{tagLabel}: noch nicht erstellt]</p>
                      : <VorschauParagraph tagId={tid} label={tagLabel} value={val} status={out?.status} isEdited={out?.is_manually_edited} quellen={out?.quellen} renderText={renderText} onSave={(t, v) => onSave(t, v, objektId)} onRegenerate={(t, e) => onRegenerate(t, e, objektId)} onOpenSource={openQuellPanel} />
                  }
                </Fragment>
              );
            })}
          </div>
        ) : (
          <p style={{ margin: 0, padding: '8px 16px', borderLeft: '3px solid #E5E7EB', color: '#9CA3AF', fontSize: 13, fontStyle: 'italic', fontFamily: 'var(--font-sans, sans-serif)' }}>
            Noch nicht generiert
          </p>
        )}

        {/* Demografie-Tabelle aus Dokument-Extraktion */}
        {demografieTabelle && (() => {
          const demDoc = projekt?.dokumente?.find(d => d.typ_raw === 'demografie' && d.extracted_fields?.fields);
          const df = demDoc?.extracted_fields?.fields;
          if (!df) return (
            <p style={{ margin: '12px 0 0', color: '#9CA3AF', fontStyle: 'italic', fontSize: 13, fontFamily: 'var(--font-sans, sans-serif)' }}>
              [Demografie-Dokument noch nicht hochgeladen oder extrahiert]
            </p>
          );
          const fmt = (v) => v != null ? Number(v).toLocaleString('de-DE') : '—';
          const fmtPct = (v) => v != null ? (v > 0 ? '+' : '') + Number(v).toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) : '—';
          const tS = { width: '100%', borderCollapse: 'collapse', marginBottom: 16, fontSize: 14, fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' };
          const tdS = { padding: '4px 12px 4px 0', borderBottom: '1px solid #E5E7EB' };
          const tdR = { ...tdS, textAlign: 'right', fontVariantNumeric: 'tabular-nums' };
          return (
            <div style={{ marginTop: 16 }}>
              <div style={{ fontSize: 14, fontWeight: 700, marginBottom: 8, fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>Bevölkerung</div>
              <table style={tS}><tbody>
                <tr><td style={tdS}>Bevölkerung insgesamt</td><td style={tdR}>{df.basisjahr || '—'}</td><td style={tdR}>{fmt(df.bevoelkerung_basisjahr)}</td></tr>
                {df.bevoelkerung_prognose_kurz != null && <tr><td style={tdS}>Bevölkerung insgesamt - vorausberechnet</td><td style={tdR}>{df.prognosejahr_kurz || '—'}</td><td style={tdR}>{fmt(df.bevoelkerung_prognose_kurz)}</td></tr>}
                {df.bevoelkerung_prognose_lang != null && <tr><td style={tdS}>Bevölkerung insgesamt - vorausberechnet</td><td style={tdR}>{df.prognosejahr_lang || '—'}</td><td style={tdR}>{fmt(df.bevoelkerung_prognose_lang)}</td></tr>}
              </tbody></table>

              <div style={{ fontSize: 14, fontWeight: 700, marginBottom: 8, fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' }}>
                Bevölkerungsveränderung {df.prognosejahr_lang || '—'} gegenüber {df.basisjahr || '—'} in Prozent
              </div>
              <table style={tS}><tbody>
                <tr><td style={tdS}>Insgesamt</td><td style={tdR}>{fmtPct(df.veraenderung_gesamt_pct)}</td></tr>
                <tr><td style={tdS}>unter 18-Jährige</td><td style={tdR}>{fmtPct(df.veraenderung_unter_18_pct)}</td></tr>
                <tr><td style={tdS}>18- bis unter 40-Jährige</td><td style={tdR}>{fmtPct(df.veraenderung_18_40_pct)}</td></tr>
                <tr><td style={tdS}>40- bis unter 65-Jährige</td><td style={tdR}>{fmtPct(df.veraenderung_40_65_pct)}</td></tr>
                <tr><td style={tdS}>65-Jährige oder Ältere</td><td style={tdR}>{fmtPct(df.veraenderung_65_plus_pct)}</td></tr>
              </tbody></table>
            </div>
          );
        })()}
      </div>
    );
  };

  return (
    <div style={{ display: isMobile ? 'block' : 'flex', gap: 24, alignItems: 'flex-start' }}>
      {/* TOC — nur Desktop (auf Mobile zu schmal, Navigation via Reiter/Scroll) */}
      {!isMobile && (
      <nav className="entwurf-vorschau-toc" style={{ position: 'sticky', top: 80, flexShrink: 0, width: 220, borderRight: '1px solid var(--border-light)', paddingRight: 16 }}>
        <div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-tertiary)', marginBottom: 10, paddingLeft: 8 }}>
          Inhaltsverzeichnis
        </div>
        {vorschauToc.map(ch => {
          const isActive = activeChapter === ch.nr;
          const isAuto = ch.type === 'auto';
          const stats = isAuto ? null : chapterStats(ch.nr);

          return (
            <button key={ch.nr} onClick={() => scrollTo(ch.nr)} title={`${ch.nr}. ${ch.label}`}
              style={{
                display: 'flex', alignItems: 'center', gap: 4, width: '100%',
                padding: '4px 8px', marginBottom: 1, background: 'none', border: 'none',
                borderLeft: isActive ? '2px solid var(--vl-blue)' : '2px solid transparent',
                cursor: 'pointer', textAlign: 'left',
                color: isAuto ? 'var(--text-tertiary)' : isActive ? 'var(--vl-blue)' : 'var(--text-secondary)',
                fontWeight: isActive ? 600 : 400, fontSize: 11.5,
                fontStyle: isAuto ? 'italic' : 'normal',
                transition: 'all 0.12s',
              }}
            >
              <span style={{ minWidth: 22, flexShrink: 0 }}>{ch.nr}.</span>
              <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ch.label}</span>
              {stats && stats.total > 0 && (
                <span style={{ fontSize: 10, fontVariantNumeric: 'tabular-nums', flexShrink: 0, color: stats.done === stats.total ? 'var(--success)' : 'var(--text-tertiary)' }}>
                  {stats.pct}%
                </span>
              )}
            </button>
          );
        })}
      </nav>
      )}

      {/* Document body */}
      <article ref={contentRef} className="card entwurf-vorschau-content"
        style={{ flex: 1, minWidth: 0, padding: isMobile ? '20px 16px' : '40px 48px', fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif', maxWidth: 820 }}
      >
        {/* ═══ Deckblatt ═══ */}
        {(() => {
          const obj = gutachten?.objekte?.[0] || {};
          const gbDoc = projekt?.dokumente?.find(d => d.typ_raw === 'grundbuchauszug' && d.extracted_fields?.fields);
          const gbF = gbDoc?.extracted_fields?.fields || {};
          const artMap = { efh: 'Wohngrundstück', etw: 'Wohngrundstück', mfh: 'Wohngrundstück', dhh: 'Wohngrundstück' };
          const otNorm = normObjekttyp(obj.objekttyp);
          const groesse = gbF.groesse_qm || obj.groesse?.replace(' m²','') || null;
          const alleObjekte = gutachten?.objekte || [];
          // Bebauung: Freitext-Beschreibung (Punkt 21) bevorzugt, sonst abgeleitet.
          // (Großbuchstaben-Vergleich 'EFH' war ein Bug — obj.objekttyp ist ein Label.)
          const bebauungMapDeck = { efh: 'Einfamilienhaus', etw: 'Eigentumswohnung', mfh: 'Mehrfamilienhaus', dhh: 'Doppelhaushälfte/Reihenhaus' };
          const bebauung = gutachten?.vorhandene_bebauung_beschreibung || [
            bebauungMapDeck[otNorm] || obj.bezeichnung || null,
            obj.wohnflaeche ? `Wohnfläche ca. ${obj.wohnflaeche} m²` : null,
          ].filter(Boolean).join(', ');

          // Deckblatt-Zeilen: bei ETW andere Labels + Inhalte (Bewertungsobjekt-
          // Beschreibung statt Objektart, Miteigentumsanteil statt Bebauung).
          // KI-Vorschlag (Punkt 14): kompakte Kurzbeschreibung der Bewertungseinheit
          const kiKurz = outputs['bewertungseinheit_kurz']?.value || null;
          let deckblattRows;
          if (otNorm === 'etw') {
            // Beschreibung des/der Bewertungsobjekte(s) — bei mehreren Einheiten
            // alle sammeln (Wohnung + Stellplätze etc.). Hauptobjekt fällt auf
            // den KI-Vorschlag zurück, wenn manuell leer.
            const beschreibungen = (alleObjekte.length > 0 ? alleObjekte : [obj])
              .map((o, idx) => o.art_lage_bewertungsobjekt || (idx === 0 ? kiKurz : null) || o.bezeichnung)
              .filter(Boolean);
            const bewObjText = beschreibungen.length > 0 ? beschreibungen.join('; ') : null;
            // Miteigentumsanteil(e): bei mehreren Einheiten alle auflisten
            const meaWerte = (alleObjekte.length > 0 ? alleObjekte : [obj])
              .map(o => o.mea)
              .filter(Boolean);
            const meaText = meaWerte.length > 0
              ? meaWerte.map(m => `${m} Miteigentumsanteil`).join(', ')
              : null;
            deckblattRows = [
              ['über das Grundstück in', gutachten?.adresse],
              ['Bewertungsobjekt', bewObjText, 'bewertungseinheit_kurz'],
              ['Anteil am Grundstück', meaText],
            ];
          } else {
            // EFH/Hof: "Bebauung" zeigt die Kurzbeschreibung — manueller Freitext,
            // sonst KI-Vorschlag (Punkt 14), sonst mechanischer Default.
            deckblattRows = [
              ['über das Grundstück in', gutachten?.adresse],
              ['Objektart', [artMap[otNorm] || obj.objekttyp, groesse ? `Größe ${groesse} m²` : null].filter(Boolean).join(', ')],
              ['Bebauung', (gutachten?.vorhandene_bebauung_beschreibung || kiKurz || bebauung) || null, 'bewertungseinheit_kurz'],
            ];
          }

          return (
            <div style={{ textAlign: 'center', marginBottom: 48, paddingBottom: 32, borderBottom: '1px solid var(--border-light)' }}>
              {/* V&L Logo */}
              <div style={{ textAlign: 'right', marginBottom: 16 }}>
                <div style={{ fontSize: 18, fontWeight: 700, letterSpacing: '0.05em', color: '#1B3A5C', fontFamily: 'var(--font-sans, sans-serif)' }}>
                  VÖLKEL<span style={{ margin: '0 2px', color: '#CBD5E1' }}>|</span>LANG
                </div>
                <div style={{ fontSize: 9, letterSpacing: '0.15em', textTransform: 'uppercase', color: '#94A3B8', fontFamily: 'var(--font-sans, sans-serif)' }}>
                  Sachverständige
                </div>
              </div>

              {/* Titel */}
              <div style={{ fontSize: 24, fontWeight: 700, letterSpacing: '0.02em', color: '#1B3A5C', marginBottom: 24 }}>
                VERKEHRSWERTGUTACHTEN
              </div>

              {/* Titelbild — klickbar zum Hochladen */}
              <AbbildungSlot
                slot={{ id: 'titelbild', label: 'Titelbild', desc: 'Objektfoto für das Deckblatt' }}
                storagePath={outputs['_abb_titelbild']?.value || gutachten?.foto_zuordnung?.foto_titelbild || null}
                gutachtenId={gutachten?.id}
                projektId={projekt?.id}
                session={session}
                workerUrl={workerUrl}
                onSave={(path) => onSave('_abb_titelbild', path)}
              />

              {/* Deckblatt-Datentabelle */}
              <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13, textAlign: 'left' }}>
                <tbody>
                  {deckblattRows.map(([lbl, val, tagId], i) => {
                    const regen = tagId && outputs[tagId];
                    const isGen = regen && outputs[tagId]?.status === 'generating';
                    return (
                    <tr key={i} style={{ borderBottom: '1px solid #E5E7EB' }}>
                      <td style={{ padding: '10px 16px 10px 0', width: '40%', color: 'var(--text-secondary)' }}>{lbl}</td>
                      <td style={{ padding: '10px 0', color: val ? 'var(--text-primary)' : '#D1D5DB', fontStyle: val ? 'normal' : 'italic', position: 'relative' }}>
                        <span style={{ paddingRight: tagId ? 30 : 0 }}>{isGen ? 'Generiert…' : (val || '[ERGÄNZEN]')}</span>
                        {tagId && (
                          <button onClick={() => onRegenerate(tagId, false)}
                            title="KI-Kurzbeschreibung neu generieren"
                            disabled={isGen}
                            style={{ position: 'absolute', top: 0, right: 0, width: 24, height: 24, borderRadius: '50%', background: 'var(--surface-light)', border: '1px solid var(--border-light)', cursor: isGen ? 'wait' : 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, color: 'var(--text-tertiary)', opacity: 0.55, transition: 'opacity 0.15s' }}
                            onMouseEnter={e => { e.currentTarget.style.opacity = '1'; e.currentTarget.style.background = 'var(--vl-blue)'; e.currentTarget.style.color = '#fff'; }}
                            onMouseLeave={e => { e.currentTarget.style.opacity = '0.55'; e.currentTarget.style.background = 'var(--surface-light)'; e.currentTarget.style.color = 'var(--text-tertiary)'; }}
                          >{'\u21BB'}</button>
                        )}
                      </td>
                    </tr>
                    );
                  })}
                  {/* Verkehrswert — hervorgehoben */}
                  <tr style={{ borderTop: '2px solid #1B3A5C', borderBottom: '2px solid #1B3A5C' }}>
                    <td style={{ padding: '10px 16px 10px 0', fontWeight: 700 }}>Verkehrswert</td>
                    <td style={{ padding: '10px 0', fontWeight: 700 }}>
                      {gutachten?.verkehrswert ? `${Number(gutachten.verkehrswert).toLocaleString('de-DE')} €` : '[ERGÄNZEN]'}
                    </td>
                  </tr>
                  {[
                    ['Wertermittlungs-/Qualitätsstichtag', gutachten?.wertermittlungsstichtag ? formatDate(gutachten.wertermittlungsstichtag) : null],
                    ['Auftraggeber', projekt?.auftraggeber],
                    ['Aktenzeichen', projekt?.akte],
                    ['Gutachten vom', gutachten?.ausfertigungsdatum
                      ? formatDate(gutachten.ausfertigungsdatum)
                      : `XX.${String(new Date().getMonth() + 1).padStart(2, '0')}.${new Date().getFullYear()}`],
                  ].map(([lbl, val], i) => (
                    <tr key={`b${i}`} style={{ borderBottom: '1px solid #E5E7EB' }}>
                      <td style={{ padding: '10px 16px 10px 0', color: 'var(--text-secondary)' }}>{lbl}</td>
                      <td style={{ padding: '10px 0', color: val ? 'var(--text-primary)' : '#D1D5DB', fontStyle: val ? 'normal' : 'italic' }}>
                        {val || '[ERGÄNZEN]'}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>

              {/* Sachverständiger */}
              {projekt?.sachverstaendiger && (
                <div style={{ marginTop: 28, textAlign: 'left', borderTop: '1px solid var(--border-light)', paddingTop: 16 }}>
                  <div style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', color: '#1B3A5C', marginBottom: 8, fontFamily: 'var(--font-sans, sans-serif)' }}>
                    Sachverständiger
                  </div>
                  <div style={{ fontSize: 13, color: 'var(--text-primary)' }}>
                    {projekt.sachverstaendiger}
                  </div>
                </div>
              )}
            </div>
          );
        })()}

        {vorschauDoc.map(block => renderBlock(block))}
      </article>

      {/* ── Quell-Abgleich Split-View ── */}
      {quellPanel && (
        <QuellPanel
          panel={quellPanel}
          notes={panelNotes}
          notesLoaded={panelNotesLoaded}
          dokumente={projekt?.dokumente || []}
          workerUrl={workerUrl}
          session={session}
          onClose={closeQuellPanel}
        />
      )}

      <style>{`
        @media (max-width: 900px) {
          .entwurf-vorschau-toc { display: none !important; }
          .entwurf-vorschau-content { padding: 24px 20px !important; max-width: 100% !important; }
        }
        @media (max-width: 1200px) {
          .entwurf-quell-panel {
            position: fixed !important; top: 0 !important; right: 0 !important;
            height: 100vh !important; max-height: 100vh !important; z-index: 200 !important;
            border-radius: 0 !important; box-shadow: -8px 0 32px rgba(0,0,0,0.18) !important;
          }
        }
        @keyframes entwurfSavedFade {
          0% { opacity: 1; }
          70% { opacity: 1; }
          100% { opacity: 0; }
        }
        @keyframes entwurfPulse {
          0%, 100% { opacity: 1; }
          50% { opacity: 0.6; }
        }
      `}</style>
    </div>
  );
};


// ── EntwurfTab Hauptkomponente ──
const EntwurfTab = ({ g, p, session, workerUrl, onRefresh }) => {
  const [outputs, setOutputs] = useState({});
  const [loading, setLoading] = useState(true);
  const [generating, setGenerating] = useState(false);
  const [progress, setProgress] = useState({ completed: 0, total: 0, phase: '', message: '' });
  const [error, setError] = useState(null);
  const generatingRef = useRef(false);
  const [checkResult, setCheckResult] = useState(null);
  const [checking, setChecking] = useState(false);
  const [exporting, setExporting] = useState(false);
  const [confirmRegenAll, setConfirmRegenAll] = useState(false);

  const runConsistencyCheck = async () => {
    setChecking(true);
    try {
      const res = await fetch(`${workerUrl}/api/entwurf/consistency-check?gutachten_id=${g.id}`, {
        headers: { Authorization: `Bearer ${session.access_token}` },
      });
      if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Fehler');
      setCheckResult(await res.json());
    } catch (e) { setError(e.message); }
    finally { setChecking(false); }
  };

  const exportDocx = async () => {
    setExporting(true);
    setError(null);
    try {
      let token = session.access_token;
      try { const sb = await initSupabase(); const { data } = await sb.auth.getSession(); if (data?.session?.access_token) token = data.session.access_token; } catch {}
      const res = await fetch(`${workerUrl}/api/entwurf/render-docx`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
        body: JSON.stringify({ gutachten_id: g.id }),
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error(err.error || err.detail || `HTTP ${res.status}`);
      }
      // DOCX-Blob als Download triggern
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = res.headers.get('Content-Disposition')?.match(/filename="?([^"]+)"?/)?.[1] || `Gutachten_${g.id}.docx`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (e) {
      console.error('[Entwurf] DOCX export failed:', e);
      setError(`DOCX-Export fehlgeschlagen: ${e.message}`);
    } finally {
      setExporting(false);
    }
  };

  // Outputs laden
  const loadOutputs = useCallback(async () => {
    try {
      const res = await fetch(`${workerUrl}/api/entwurf/outputs?gutachten_id=${g.id}`, {
        headers: { Authorization: `Bearer ${session.access_token}` },
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      // Nicht überschreiben wenn gerade generiert wird — SSE-Stream ist aktueller
      if (!generatingRef.current) {
        setOutputs(data.outputs || {});
      }
      setError(null);
    } catch (e) {
      console.error('[Entwurf] Load failed:', e);
      setError('Entwurf-Daten konnten nicht geladen werden.');
    } finally {
      setLoading(false);
    }
  }, [g.id, session.access_token, workerUrl]);

  useEffect(() => { loadOutputs(); }, [g.id]); // eslint-disable-line react-hooks/exhaustive-deps

  // Bulk-Generierung mit SSE-Stream
  const startBulkGenerate = async () => {
    if (generatingRef.current) return;
    generatingRef.current = true;
    setGenerating(true);
    setProgress({ completed: 0, total: 0, phase: 'starting', message: 'Vorbereitung...' });
    setError(null);

    let completedNaturally = false;

    try {
      const response = await fetch(`${workerUrl}/api/entwurf/generate-batch`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${session.access_token}`,
        },
        body: JSON.stringify({ gutachten_id: g.id }),
      });

      if (!response.ok) {
        const err = await response.json().catch(() => ({}));
        throw new Error(err.error || `HTTP ${response.status}`);
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';

        for (const line of lines) {
          // SSE-Comments (Heartbeats) ignorieren
          if (line.startsWith(':')) continue;
          if (!line.startsWith('data: ')) continue;
          let event;
          try { event = JSON.parse(line.substring(6)); } catch { continue; }

          if (event.type === 'phase') {
            setProgress(prev => ({
              ...prev,
              phase: event.phase,
              message: event.message || '',
              ...(event.totalTags ? { total: event.totalTags } : {}),
            }));
          } else if (event.type === 'batch_results') {
            const newEntries = {};
            for (const [tid, val] of Object.entries(event.results || {})) {
              const key = outputKey(tid, event.objektId || null);
              newEntries[key] = { value: val, status: 'generated', is_manually_edited: false, generated_at: new Date().toISOString(), objekt_id: event.objektId || null };
            }
            setOutputs(prev => ({ ...prev, ...newEntries }));
            setProgress(prev => ({ ...prev, completed: event.completedCount || prev.completed }));
          } else if (event.type === 'batch_error') {
            const errorEntries = {};
            for (const tid of (event.failedTags || [])) {
              const key = outputKey(tid, event.objektId || null);
              errorEntries[key] = { status: 'error', error_message: event.error };
            }
            setOutputs(prev => ({ ...prev, ...errorEntries }));
          } else if (event.type === 'complete') {
            completedNaturally = true;
            setProgress(prev => ({ ...prev, phase: 'complete', message: 'Fertig!' }));
          } else if (event.type === 'error') {
            setError(event.message);
          }
        }
      }
    } catch (e) {
      if (!completedNaturally) {
        // Verbindung wurde unterbrochen (Tab-Wechsel, Netzwerk).
        // Die Generierung läuft serverseitig weiter → per Polling den Fortschritt verfolgen.
        console.warn('[Entwurf] Stream unterbrochen:', e.message);
        setProgress(prev => ({ ...prev, phase: 'reconnecting', message: 'Verbindung unterbrochen — lade Ergebnisse...' }));

        // Polling: alle 5s Outputs nachladen bis alles fertig ist (max 2 Minuten)
        const pollStart = Date.now();
        const expectedTotal = progress.total || 129;
        while (Date.now() - pollStart < 120000) {
          await new Promise(r => setTimeout(r, 5000));
          try {
            const res = await fetch(`${workerUrl}/api/entwurf/outputs?gutachten_id=${g.id}`, {
              headers: { Authorization: `Bearer ${session.access_token}` },
            });
            if (!res.ok) continue;
            const data = await res.json();
            const newOutputs = data.outputs || {};
            setOutputs(newOutputs);
            const generated = Object.values(newOutputs).filter(o => o.status === 'generated' || o.status === 'edited').length;
            const pending = Object.values(newOutputs).filter(o => o.status === 'generating' || o.status === 'pending').length;
            setProgress(prev => ({
              ...prev,
              completed: generated,
              message: pending > 0
                ? `Verbindung unterbrochen — ${generated} Bausteine fertig, ${pending} noch ausstehend...`
                : 'Alle Bausteine geladen!',
            }));
            // Fertig wenn keine Tags mehr auf 'generating' oder 'pending' stehen
            if (pending === 0) {
              setProgress(prev => ({ ...prev, phase: 'complete', message: 'Fertig!' }));
              completedNaturally = true;
              break;
            }
          } catch { /* Polling-Fehler ignorieren, nächster Versuch in 5s */ }
        }
      } else {
        console.error('[Entwurf] Generate failed:', e);
        setError(e.message || 'Generierung fehlgeschlagen');
        try { await loadOutputs(); } catch {}
      }
    } finally {
      generatingRef.current = false;
      if (completedNaturally) {
        await new Promise(r => setTimeout(r, 1500));
      }
      setGenerating(false);
    }
  };

  // Einzelnen Tag editieren
  const handleSave = async (tagId, value, objektId = null) => {
    const key = outputKey(tagId, objektId);
    const payload = { gutachten_id: g.id, tag_id: tagId, value };
    if (objektId) payload.objekt_id = objektId;
    const res = await fetch(`${workerUrl}/api/entwurf/output`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
      body: JSON.stringify(payload),
    });
    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      throw new Error(err.error || 'Speichern fehlgeschlagen');
    }
    setOutputs(prev => ({
      ...prev,
      [key]: { ...prev[key], value, status: 'edited', is_manually_edited: true, edited_at: new Date().toISOString() },
    }));
  };

  // Einzelnen Tag regenerieren
  const handleRegenerate = async (tagId, isManuallyEdited, objektId = null) => {
    const key = outputKey(tagId, objektId);
    setOutputs(prev => ({ ...prev, [key]: { ...prev[key], status: 'generating' } }));
    try {
      const payload = { gutachten_id: g.id, tag_id: tagId, force: isManuallyEdited };
      if (objektId) payload.objekt_id = objektId;
      const res = await fetch(`${workerUrl}/api/entwurf/regenerate-tag`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
        body: JSON.stringify(payload),
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error(err.error || 'Regenerieren fehlgeschlagen');
      }
      const data = await res.json();
      setOutputs(prev => ({
        ...prev,
        [key]: { ...prev[key], value: data.value, status: 'generated', is_manually_edited: false, generated_at: new Date().toISOString() },
      }));
    } catch (e) {
      setOutputs(prev => ({ ...prev, [key]: { ...prev[key], status: 'error', error_message: e.message } }));
      throw e;
    }
  };

  // Stats
  const totalTags = ENTWURF_KAPITEL.reduce((sum, k) => sum + k.subkapitel.reduce((s2, sub) => s2 + sub.aiTags.length, 0), 0);
  // Nur Tags zählen die in ENTWURF_KAPITEL definiert sind — _kap1_/_kap2_ Override-Tags ausschließen
  const entwurfTagSet = useMemo(() => {
    const s = new Set();
    for (const kap of ENTWURF_KAPITEL) for (const sub of kap.subkapitel) for (const t of sub.aiTags) s.add(t);
    return s;
  }, []);
  const doneTags = Object.entries(outputs).filter(([key, o]) =>
    (o.status === 'generated' || o.status === 'edited') && entwurfTagSet.has(key.split('::')[0])
  ).length;
  const pctDone = generating && progress.total > 0
    ? Math.min(100, Math.round((progress.completed / progress.total) * 100))
    : doneTags > 0 ? 100 : 0;

  // Offene (noch nicht erstellte) Bausteine + Gesamtfortschritt in Prozent.
  // Prozent statt Roh-Bruch im Ring: „93 %" liest sich wie „fast fertig",
  // „128/138" wie „10 fehlen → unfertig". Das war die Verwirrung.
  const offen = Math.max(0, totalTags - doneTags);
  const pctDisplay = generating && progress.total > 0
    ? Math.min(100, Math.round((progress.completed / progress.total) * 100))
    : (totalTags > 0 ? Math.round((doneTags / totalTags) * 100) : 0);

  // Generierungs-Cockpit: Fortschritts-Ring + Status-Pille (gleiche Ring-Sprache wie Ortstermin/Übersicht)
  const ringTotal = generating ? (progress.total || totalTags || 0) : (totalTags || 0);
  const ringDone = generating ? Math.min(progress.completed || 0, ringTotal) : doneTags;
  const ringPct = ringTotal > 0 ? ringDone / ringTotal : 0;
  const ringC = 2 * Math.PI * 20;
  const ringColor = generating ? 'var(--vl-blue)'
    : (totalTags > 0 && doneTags >= totalTags) ? 'var(--success)'
    : doneTags > 0 ? 'var(--vl-orange)' : 'var(--text-tertiary)';
  const statusPill = generating
    ? { label: 'Generierung läuft…', fg: 'var(--vl-blue)', bg: 'var(--surface-blue)' }
    : doneTags === 0
      ? { label: 'Noch nicht generiert', fg: 'var(--text-secondary)', bg: 'var(--surface-light)' }
      : offen === 0
        ? { label: 'Vollständig', fg: 'var(--success)', bg: 'var(--success-bg)' }
        : { label: `${offen} Stelle${offen === 1 ? '' : 'n'} offen`, fg: 'var(--warning)', bg: 'var(--warning-bg)' };

  // Loading
  if (loading) {
    return (
      <div className="card" style={{ textAlign: 'center', padding: 'var(--space-10)', color: 'var(--text-tertiary)' }}>
        <span className="entwurf-spinner large" style={{ display: 'inline-block', marginBottom: 'var(--space-3)' }} />
        <div>Entwurf wird geladen...</div>
      </div>
    );
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }}>
      {/* Header */}
      <div className="card">
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--space-4)', flexWrap: 'wrap' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)', minWidth: 0 }}>
            <span style={{ position: 'relative', width: 48, height: 48, flexShrink: 0 }} title={`${ringDone} von ${ringTotal} Textbausteinen erstellt`}>
              <svg width="48" height="48" viewBox="0 0 48 48" style={{ transform: 'rotate(-90deg)' }}>
                <circle cx="24" cy="24" r="20" fill="none" stroke="var(--border-light)" strokeWidth="4" />
                <circle cx="24" cy="24" r="20" fill="none" stroke={ringColor} strokeWidth="4" strokeLinecap="round"
                  strokeDasharray={ringC} strokeDashoffset={ringC * (1 - ringPct)}
                  style={{ transition: 'stroke-dashoffset var(--dur-slow) var(--ease-out), stroke var(--dur-base)' }} />
              </svg>
              <span style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, color: ringColor, fontVariantNumeric: 'tabular-nums' }}>{pctDisplay}%</span>
            </span>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 5, display: 'flex', alignItems: 'center', gap: 8 }}>
                Gutachten-Entwurf
                <HelpTip text="Klicke auf einen Absatz, um ihn direkt zu bearbeiten — das ist der schnellste Weg für Korrekturen. ↻ generiert einen einzelnen Baustein neu (kostet Tokens und überschreibt manuelle Änderungen). Mit [ERGÄNZEN] markierte Stellen fehlen noch Informationen." />
              </div>
              <span className="pill" style={{ color: statusPill.fg, background: statusPill.bg }}>{statusPill.label}</span>
            </div>
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
            {doneTags === 0 ? (
              <button className="btn btn-accent" onClick={startBulkGenerate} disabled={generating}>
                {generating ? 'Generierung läuft…' : 'Entwurf generieren'}
              </button>
            ) : (
              <>
                <button className="btn btn-ghost" onClick={runConsistencyCheck} disabled={checking || generating}
                  style={{ fontSize: 12 }}>
                  {checking ? 'Prüfe…' : 'Konsistenz prüfen'}
                </button>
                <button className="btn btn-accent" onClick={exportDocx} disabled={exporting || generating}
                  style={{ fontSize: 12 }}>
                  {exporting ? 'Exportiere…' : 'DOCX exportieren'}
                </button>
                <button className="btn btn-ghost" onClick={() => setConfirmRegenAll(true)} disabled={generating}
                  title="Alle Bausteine neu generieren — überschreibt manuelle Änderungen und kostet Tokens"
                  style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                  {generating ? 'Generierung läuft…' : '↻ Neu generieren'}
                </button>
              </>
            )}
          </div>
        </div>

        {generating && (
          <div style={{ marginTop: 'var(--space-3)', padding: '12px 16px', background: 'var(--surface-light)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-light)' }}>
            {/* Phase steps */}
            <div style={{ display: 'flex', gap: 16, marginBottom: 10, fontSize: 12, fontVariantNumeric: 'tabular-nums' }}>
              {[
                { id: 'loading', label: 'Laden' },
                { id: 'templates', label: 'Vorlagen' },
                { id: 'phase1', label: 'Stammdaten' },
                { id: 'phase2', label: 'Ortstermin' },
                { id: 'phase3', label: 'Beurteilungen' },
              ].map(step => {
                const phases = ['loading', 'loaded', 'templates', 'phase1', 'phase2', 'phase3', 'complete'];
                const currentIdx = phases.indexOf(progress.phase);
                const stepIdx = phases.indexOf(step.id);
                const isDone = currentIdx > stepIdx;
                const isCurrent = progress.phase === step.id || (step.id === 'phase1' && progress.phase === 'loaded');
                return (
                  <div key={step.id} style={{ display: 'flex', alignItems: 'center', gap: 4, color: isDone ? 'var(--success)' : isCurrent ? 'var(--vl-blue)' : 'var(--text-tertiary)' }}>
                    {isDone ? '✓' : isCurrent ? <span className="entwurf-spinner" style={{ width: 12, height: 12 }} /> : '○'}
                    <span style={{ fontWeight: isCurrent ? 600 : 400 }}>{step.label}</span>
                  </div>
                );
              })}
            </div>

            {/* Current action + counter */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--text-secondary)' }}>
              <span>{progress.message || 'Generierung läuft...'}</span>
              {progress.total > 0 && (
                <span style={{ marginLeft: 'auto', fontWeight: 600, fontVariantNumeric: 'tabular-nums', color: 'var(--vl-blue)' }}>
                  {Math.min(progress.completed, progress.total)} / {progress.total}
                </span>
              )}
            </div>
          </div>
        )}

        {error && (
          <div style={{ marginTop: 'var(--space-3)', padding: 'var(--space-2) var(--space-3)', background: 'rgba(220,38,38,0.06)', borderRadius: 'var(--radius-sm)', color: 'var(--danger)', fontSize: 13 }}>
            {error}
          </div>
        )}
      </div>

      {/* Empty State */}
      {doneTags === 0 && !generating && (
        <div className="card" style={{ textAlign: 'center', padding: 'var(--space-8) var(--space-4)' }}>
          <div style={{ fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 'var(--space-2)' }}>
            Noch kein Entwurf vorhanden
          </div>
          <div style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>
            Klicke oben auf "Entwurf generieren", um aus Stammdaten und Ortstermin-Notizen
            alle Textbausteine automatisch zu erstellen.
          </div>
        </div>
      )}

      {/* Konsistenz-Check Ergebnisse */}
      {checkResult && (
        <div style={{ marginTop: 'var(--space-3)', padding: 16, background: 'var(--warning-bg)', border: '1px solid var(--warning)', borderRadius: 'var(--radius-md)', fontSize: 13 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
            <div style={{ fontWeight: 700, color: 'var(--warning)' }}>
              Konsistenz-Check: {checkResult.summary.with_ergaenzen} Lücken gefunden
              {checkResult.summary.fixable_by_regenerate > 0 && (
                <span style={{ fontWeight: 400, marginLeft: 8 }}>
                  ({checkResult.summary.fixable_by_regenerate} davon durch Regenerieren behebbar)
                </span>
              )}
            </div>
            <button className="btn btn-ghost" onClick={() => setCheckResult(null)}
              style={{ fontSize: 11, padding: '2px 8px' }}>×</button>
          </div>

          {checkResult.issues.filter(i => i.available_fixes.length > 0).length > 0 && (
            <div style={{ marginBottom: 12 }}>
              <div style={{ fontWeight: 600, color: 'var(--warning)', marginBottom: 6, fontSize: 12 }}>
                Daten vorhanden, aber nicht im Entwurf:
              </div>
              {checkResult.issues.filter(i => i.available_fixes.length > 0).map(issue => (
                <div key={issue.tag_id} style={{ padding: '6px 10px', marginBottom: 4, background: 'var(--surface)', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
                  <div>
                    <span style={{ fontWeight: 600 }}>{entwurfTagLabel(issue.tag_id)}</span>
                    <span style={{ marginLeft: 8, color: 'var(--text-secondary)' }}>
                      {issue.available_fixes.map(f => `${f.field}: ${f.value}`).join(', ')}
                    </span>
                  </div>
                  <span className="pill" style={{ color: 'var(--warning)', background: 'var(--warning-bg)', fontSize: 11, whiteSpace: 'nowrap' }}>↻ regenerierbar</span>
                </div>
              ))}
            </div>
          )}

          {checkResult.issues.filter(i => i.hints.length > 0 && i.available_fixes.length === 0).length > 0 && (
            <div>
              <div style={{ fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 6, fontSize: 12 }}>
                Manuell zu ergänzen:
              </div>
              {checkResult.issues.filter(i => i.hints.length > 0 && i.available_fixes.length === 0).map(issue => (
                <div key={issue.tag_id} style={{ padding: '4px 10px', marginBottom: 3, fontSize: 12, color: 'var(--text-tertiary)' }}>
                  <span style={{ fontWeight: 600 }}>{entwurfTagLabel(issue.tag_id)}</span>: {issue.hints[0]}
                </div>
              ))}
            </div>
          )}
        </div>
      )}

      {/* Gutachten-Vorschau — immer sichtbar, Kap. 1+2 kommen aus Stammdaten */}
      <EntwurfVorschau
        outputs={outputs}
        projekt={p}
        gutachten={g}
        session={session}
        workerUrl={workerUrl}
        onSave={handleSave}
        onRegenerate={handleRegenerate}
      />
      {confirmRegenAll && (
        <ConfirmDialog
          title="Alle Bausteine neu generieren?"
          message={`Alle ${totalTags} Textbausteine werden neu erzeugt. Manuelle Änderungen gehen dabei verloren, und es kostet Tokens und etwas Zeit. Für einzelne Korrekturen ist direktes Bearbeiten schneller.`}
          confirmLabel="Alle neu generieren"
          onConfirm={() => { setConfirmRegenAll(false); startBulkGenerate(); }}
          onCancel={() => setConfirmRegenAll(false)}
        />
      )}
    </div>
  );
};

// ════════════════ BAUSCHADEN-KOMPONENTEN ════════════════

// ════════════════════════════════════════════════════════════════════
// BeweisfragenTab — Bauschaden-Pendant zum StammdatenTab
// ════════════════════════════════════════════════════════════════════
// Einfügen in vl-app.jsx in der Nähe von StammdatenTab. Verwendet bewusst
// die VORHANDENEN Bausteine aus dem Modul-Scope:
//   - EditableField                         (Inline-Edit, click-to-save)
//   - apiInsertRow / apiPatchRow / apiDeleteRow   (CRUD über /api/row)
//   - dieselben CSS-Variablen wie StammdatenSektion (--surface, --border-…,
//     --radius-…, --shadow-…, --space-…, --text-…, --success/--warning)
// React-Hooks (useState/useMemo/useEffect) sind im Modul bereits importiert.
//
// Phase 1 (Auftragsanlage): REVIEW der aus dem Beweisbeschluss extrahierten
// Fragen — Behauptung bearbeiten, sortieren, Gewerk/Zuständigkeit setzen,
// Ergänzungsfragen anlegen (Ortstermin-Fall I.16), löschen.
// Die ANTWORTfelder (feststellungen/stellungnahme/ergebnis) gehören in
// Phase 4 (Entwurf) — Platzhalter am Kartenende markiert.
//
// Einhängen: bei g.art === 'bauschaden' statt StammdatenTab (siehe
// 05_integration-beweisfragen-tab.md).
// ════════════════════════════════════════════════════════════════════

// UI-Bequemlichkeit, KEIN Constraint — die Liste ist beliebig erweiterbar.
// Das Gewerk bleibt ein freies Textfeld in der DB; hier nur Vorschläge.
const GEWERK_OPTIONS = [
  { value: 'fliesenleger',     label: 'Fliesen- und Plattenleger' },
  { value: 'heizung_sanitaer', label: 'Heizung / Sanitär' },
  { value: 'schreiner',        label: 'Schreiner' },
  { value: 'parkett_boden',    label: 'Parkett / Bodenleger' },
  { value: 'maler',            label: 'Maler / Lackierer' },
  { value: 'zimmerer',         label: 'Zimmerer / Holzbau' },
  { value: 'dachdecker',       label: 'Dachdecker' },
  { value: 'maurer',           label: 'Maurer / Beton' },
  { value: 'elektro',          label: 'Elektro' },
];

const QUELLE_PILL = {
  beschluss:  { label: 'Beschluss',  bg: 'var(--surface-light)', color: 'var(--text-tertiary)' },
  ergaenzung: { label: 'Ergänzung',  bg: 'var(--warning-bg)',    color: 'var(--warning)' },
};

const BeweisfragenTab = ({
  g, herkunft,
  session, workerUrl,
  onRefresh, pendingFeld, onFeldConsumed,
}) => {
  const fragen = useMemo(
    () => [...(g.beweisfragen || [])].sort((a, b) => (a.sortier_index ?? 0) - (b.sortier_index ?? 0)),
    [g.beweisfragen]
  );
  const canEdit = !!session && !!g.id;

  const [busyId, setBusyId]               = useState(null);
  const [addBusy, setAddBusy]             = useState(false);
  const [error, setError]                 = useState(null);
  const [deleteConfirm, setDeleteConfirm] = useState(null);

  const reload = () => onRefresh && onRefresh();

  // Sprunganforderung aus der Befehls-Palette ggf. konsumieren (kein Jump in v1)
  useEffect(() => { if (pendingFeld) onFeldConsumed && onFeldConsumed(); }, [pendingFeld]);

  // ── CRUD ──────────────────────────────────────────────────────────
  const updateFrage = async (id, feld, wert) => {
    const numeric = new Set(['sortier_index']);
    const val = numeric.has(feld) && wert != null && wert !== '' ? Number(wert)
              : (wert === '' ? null : wert);
    await apiPatchRow('beweisfragen', id, { [feld]: val }, session, workerUrl);
    reload();
  };

  const toggleZustaendig = async (frage) => {
    setBusyId(frage.id); setError(null);
    try {
      await apiPatchRow('beweisfragen', frage.id, { zustaendig: !frage.zustaendig }, session, workerUrl);
      reload();
    } catch (e) { setError(e.message || String(e)); }
    finally { setBusyId(null); }
  };

  // Reihenfolge ändern: sortier_index mit dem Nachbarn tauschen
  const moveFrage = async (idx, dir) => {
    const a = fragen[idx], b = fragen[idx + dir];
    if (!a || !b) return;
    setBusyId(a.id); setError(null);
    try {
      await apiPatchRow('beweisfragen', a.id, { sortier_index: b.sortier_index ?? 0 }, session, workerUrl);
      await apiPatchRow('beweisfragen', b.id, { sortier_index: a.sortier_index ?? 0 }, session, workerUrl);
      reload();
    } catch (e) { setError(e.message || String(e)); }
    finally { setBusyId(null); }
  };

  const addErgaenzung = async () => {
    setAddBusy(true); setError(null);
    try {
      const maxIdx = fragen.reduce((m, f) => Math.max(m, f.sortier_index ?? 0), 0);
      await apiInsertRow('beweisfragen', {
        gutachten_id:  g.id,
        project_id:    g.project_id || null,
        sortier_index: maxIdx + 1,
        nummer:        '',
        quelle:        'ergaenzung',
        behauptung:    '',
        referenzen:    [],
        gewerk:        g.gewerk || null,
        zustaendig:    true,
        status:        'offen',
      }, session, workerUrl);
      reload();
    } catch (e) { setError(e.message || String(e)); }
    finally { setAddBusy(false); }
  };

  const executeDelete = async () => {
    if (!deleteConfirm) return;
    setDeleteConfirm(s => ({ ...s, busy: true, error: null }));
    try {
      await apiDeleteRow('beweisfragen', deleteConfirm.frage.id, session, workerUrl);
      setDeleteConfirm(null);
      reload();
    } catch (e) { setDeleteConfirm(s => ({ ...s, busy: false, error: e.message || String(e) })); }
  };

  const zustaendigCount = fragen.filter(f => f.zustaendig).length;
  const gewerkLabel = (slug) => (GEWERK_OPTIONS.find(o => o.value === slug)?.label) || slug || null;

  // ── Empty State ───────────────────────────────────────────────────
  if (!fragen.length) {
    return (
      <div style={{ padding: 'var(--space-6, 32px)', textAlign: 'center', color: 'var(--text-tertiary)' }}>
        <p style={{ fontSize: 15, color: 'var(--text-secondary)', marginBottom: 8 }}>
          Noch keine Beweisfragen erfasst.
        </p>
        <p style={{ fontSize: 13, marginBottom: 20 }}>
          Lade den Beweisbeschluss unter „Unterlagen" hoch und übernimm die Extraktion —
          die Fragen erscheinen dann hier. Oder lege manuell eine Frage an.
        </p>
        {canEdit && (
          <button type="button" onClick={addErgaenzung} disabled={addBusy} className="btn-secondary">
            {addBusy ? 'Wird angelegt…' : '+ Frage manuell anlegen'}
          </button>
        )}
        {error && <p style={{ color: 'var(--danger, #c0392b)', fontSize: 13, marginTop: 12 }}>{error}</p>}
      </div>
    );
  }

  // ── Liste ─────────────────────────────────────────────────────────
  return (
    <div>
      {/* Hero / Übersicht */}
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap',
        gap: 'var(--space-3)', marginBottom: 'var(--space-4, 20px)',
      }}>
        <div>
          <div style={{ fontSize: 18, fontWeight: 650, color: 'var(--text-primary)' }}>
            Beweisfragen
          </div>
          <div style={{ fontSize: 13, color: 'var(--text-tertiary)', marginTop: 2 }}>
            {fragen.length} {fragen.length === 1 ? 'Frage' : 'Fragen'}
            {zustaendigCount !== fragen.length && <> · {zustaendigCount} zu bearbeiten</>}
            {g.gewerk && <> · Gewerk: {gewerkLabel(g.gewerk)}</>}
          </div>
        </div>
        {canEdit && (
          <button type="button" onClick={addErgaenzung} disabled={addBusy} className="btn-secondary">
            {addBusy ? 'Wird angelegt…' : '+ Ergänzungsfrage'}
          </button>
        )}
      </div>

      {error && (
        <div style={{
          background: 'var(--danger-bg, #fdecea)', color: 'var(--danger, #c0392b)',
          border: '1px solid var(--danger, #c0392b)', borderRadius: 'var(--radius-md)',
          padding: '8px 12px', fontSize: 13, marginBottom: 'var(--space-3)',
        }}>{error}</div>
      )}

      {/* Fragenkarten */}
      {fragen.map((frage, idx) => {
        const pill = QUELLE_PILL[frage.quelle] || QUELLE_PILL.beschluss;
        const dim = !frage.zustaendig;
        const oh = (feld) => herkunft?.[`beweisfragen:${frage.id}:${feld}`];
        const behauptungHerkunft = oh('behauptung');

        return (
          <div key={frage.id} style={{
            position: 'relative',
            marginBottom: 'var(--space-3)', background: 'var(--surface)',
            border: '1px solid var(--border-light)', borderRadius: 'var(--radius-lg)',
            boxShadow: 'var(--shadow-subtle)', overflow: 'hidden',
            opacity: dim ? 0.55 : 1, transition: 'opacity var(--dur-base) var(--ease-out)',
          }}>
            {/* Kopfzeile: Nummer, Quelle, Sortier-Pfeile */}
            <div style={{
              display: 'flex', alignItems: 'center', gap: 'var(--space-3)',
              padding: '11px 14px', borderBottom: '1px solid var(--border-light)',
              background: 'var(--surface-raised)',
            }}>
              <div style={{ fontWeight: 650, fontSize: 14, color: 'var(--text-primary)', minWidth: 56 }}>
                <EditableField
                  value={frage.nummer}
                  onSave={(v) => updateFrage(frage.id, 'nummer', v)}
                  placeholder="Nr."
                  disabled={!canEdit}
                  inputStyle={{ width: 70 }}
                />
              </div>
              <span style={{
                fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 999,
                background: pill.bg, color: pill.color,
              }}>{pill.label}</span>

              <span style={{ flex: 1 }} />

              {/* Sortieren */}
              <button type="button" title="Nach oben" disabled={!canEdit || idx === 0 || busyId === frage.id}
                onClick={() => moveFrage(idx, -1)} className="icon-btn"
                style={{ opacity: idx === 0 ? 0.3 : 1 }}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="18 15 12 9 6 15" /></svg>
              </button>
              <button type="button" title="Nach unten" disabled={!canEdit || idx === fragen.length - 1 || busyId === frage.id}
                onClick={() => moveFrage(idx, 1)} className="icon-btn"
                style={{ opacity: idx === fragen.length - 1 ? 0.3 : 1 }}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9" /></svg>
              </button>
            </div>

            {/* Behauptung */}
            <div style={{ padding: '12px 14px' }}>
              <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.3, marginBottom: 4 }}>
                Behauptung / Frage
              </div>
              <div style={{ fontSize: 14, lineHeight: 1.5, color: 'var(--text-primary)' }}>
                <EditableField
                  value={frage.behauptung}
                  onSave={(v) => updateFrage(frage.id, 'behauptung', v)}
                  type="textarea"
                  placeholder="Behauptung aus dem Beweisbeschluss…"
                  disabled={!canEdit}
                  style={{ display: 'block', width: '100%' }}
                  inputStyle={{ width: '100%', minHeight: 70 }}
                />
              </div>
              {behauptungHerkunft && (
                <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>
                  Quelle: {behauptungHerkunft.label || 'Beweisbeschluss'}
                </div>
              )}

              {/* Referenzen */}
              {(frage.referenzen || []).length > 0 && (
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }}>
                  {(frage.referenzen || []).map((r, i) => (
                    <span key={i} style={{
                      fontSize: 11, padding: '2px 8px', borderRadius: 6,
                      background: 'var(--surface-light)', color: 'var(--text-secondary)',
                      border: '1px solid var(--border-light)',
                    }}>
                      {r.quelle}{r.fundstelle ? ` · ${r.fundstelle}` : ''}
                    </span>
                  ))}
                </div>
              )}
            </div>

            {/* Fußzeile: Gewerk, Zuständigkeit, Löschen */}
            <div style={{
              display: 'flex', alignItems: 'center', gap: 'var(--space-3)', flexWrap: 'wrap',
              padding: '10px 14px', borderTop: '1px solid var(--border-light)',
            }}>
              <label style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Gewerk:</label>
              <div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
                <EditableField
                  value={frage.gewerk}
                  onSave={(v) => updateFrage(frage.id, 'gewerk', v)}
                  type="select"
                  options={[{ value: '', label: '— offen —' }, ...GEWERK_OPTIONS]}
                  format={(v) => gewerkLabel(v) || '— offen —'}
                  placeholder="— offen —"
                  disabled={!canEdit}
                />
              </div>

              <span style={{ flex: 1 }} />

              {/* Zuständig-Schalter */}
              <button type="button" onClick={() => toggleZustaendig(frage)} disabled={!canEdit || busyId === frage.id}
                title={frage.zustaendig ? 'Wird von mir bearbeitet — klicken zum Ausblenden' : 'Anderes Gewerk — klicken zum Übernehmen'}
                style={{
                  display: 'inline-flex', alignItems: 'center', gap: 6, cursor: canEdit ? 'pointer' : 'default',
                  fontSize: 12, fontWeight: 600, padding: '4px 10px', borderRadius: 999, border: '1px solid',
                  borderColor: frage.zustaendig ? 'var(--success)' : 'var(--border-light)',
                  background: frage.zustaendig ? 'var(--success-bg)' : 'var(--surface-light)',
                  color: frage.zustaendig ? 'var(--success)' : 'var(--text-tertiary)',
                }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
                  {frage.zustaendig ? <polyline points="20 6 9 17 4 12" /> : <line x1="5" y1="12" x2="19" y2="12" />}
                </svg>
                {frage.zustaendig ? 'Zuständig' : 'Anderes Gewerk'}
              </button>

              <button type="button" title="Frage löschen" disabled={!canEdit}
                onClick={() => setDeleteConfirm({ frage, busy: false, error: null })} className="icon-btn"
                style={{ color: 'var(--text-tertiary)' }}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg>
              </button>
            </div>

            {/* ── Phase 4 (Entwurf): hier kommen Feststellungen / Stellungnahme /
                 Ergebnis + Mangel-Bewertung hin. In der Auftragsanlage bewusst
                 ausgeblendet. ── */}
          </div>
        );
      })}

      {/* Lösch-Bestätigung */}
      {deleteConfirm && (
        <div style={{
          position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 1000,
          display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
        }} onClick={() => !deleteConfirm.busy && setDeleteConfirm(null)}>
          <div onClick={(e) => e.stopPropagation()} style={{
            background: 'var(--surface)', borderRadius: 'var(--radius-lg)', padding: 24,
            maxWidth: 440, width: '100%', boxShadow: 'var(--shadow-lg, 0 10px 40px rgba(0,0,0,0.2))',
          }}>
            <div style={{ fontSize: 16, fontWeight: 650, color: 'var(--text-primary)', marginBottom: 8 }}>
              Beweisfrage löschen?
            </div>
            <div style={{ fontSize: 14, color: 'var(--text-secondary)', marginBottom: 4 }}>
              {deleteConfirm.frage.nummer ? <strong>{deleteConfirm.frage.nummer} </strong> : null}
              {(deleteConfirm.frage.behauptung || '').slice(0, 120)}
              {(deleteConfirm.frage.behauptung || '').length > 120 ? '…' : ''}
            </div>
            <div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 16 }}>
              Tipp: Gehört die Frage zu einem anderen Gewerk, sie besser auf „Anderes Gewerk" setzen statt löschen.
            </div>
            {deleteConfirm.error && <div style={{ color: 'var(--danger, #c0392b)', fontSize: 13, marginBottom: 12 }}>{deleteConfirm.error}</div>}
            <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
              <button type="button" className="btn-secondary" disabled={deleteConfirm.busy} onClick={() => setDeleteConfirm(null)}>Abbrechen</button>
              <button type="button" className="btn-danger" disabled={deleteConfirm.busy} onClick={executeDelete}>
                {deleteConfirm.busy ? 'Wird gelöscht…' : 'Löschen'}
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

// ════════════════════════════════════════════════════════════════════
// BeweisfragenOrtsterminTab — Ortstermin für art='bauschaden'
// ════════════════════════════════════════════════════════════════════
// Einfügen in vl-app.jsx nahe OrtsterminTab. Lässt den 970-Zeilen-VWG-
// OrtsterminTab UNBERÜHRT; die Verzweigung läuft in GutachtenView über
// g.art (siehe Integration unten).
//
// Wiederverwendete Modul-Bausteine (alle bereits vorhanden):
//   NoteCard, ladeNotes, insertNote, updateNote, deleteNote, uploadPhotoBlob
//   (uploadPhotoBlob → { photo_id, storage_path, size }; storage_path == image_url)
//   React-Hooks bare (useState/…), wie überall im Modul.
//
// Fokus von 2b: ERFASSUNG (Text + Foto) und ZUORDNUNG Notiz→Beweisfrage,
// plus Abdeckungs-Übersicht (welche Frage hat schon Befunde). Die
// Sprach-/KI-Split-Erfassung des VWG-Tabs (useVoice/Rundgang) ist agnostisch
// und kann später hier eingehängt werden — bewusst noch nicht dupliziert.
// ════════════════════════════════════════════════════════════════════

const BeweisfragenOrtsterminTab = ({ p, g, session, workerUrl, userProfile, onRefresh }) => {
  // Nur zuständige Fragen sind Zuordnungsziele
  const fragen = useMemo(
    () => [...(g.beweisfragen || [])]
      .filter(f => f.zustaendig !== false)
      .sort((a, b) => (a.sortier_index ?? 0) - (b.sortier_index ?? 0)),
    [g.beweisfragen]
  );

  const [notes, setNotes]       = useState([]);
  const [loading, setLoading]   = useState(false);
  const [draft, setDraft]       = useState('');
  const [saving, setSaving]     = useState(false);
  const [photoBusy, setPhotoBusy] = useState(false);
  const [error, setError]       = useState(null);
  const photoInputRef = useRef(null);

  const projektId = g.project_id || p?.id || null;

  const reload = useCallback(async () => {
    if (!g.id) return;
    setLoading(true);
    try { setNotes(await ladeNotes(g.id)); }
    catch (e) { console.warn('[BeweisfragenOrtstermin] notes load:', e.message); }
    finally { setLoading(false); }
  }, [g.id]);
  useEffect(() => { reload(); }, [reload]);

  // ── Erfassen ──────────────────────────────────────────────────────
  const addTextNote = async () => {
    const text = draft.trim();
    if (!text) return;
    setSaving(true); setError(null);
    try {
      await insertNote({
        project_id: projektId, gutachten_id: g.id,
        type: 'text', text, beweisfrage_id: null,
        user_id: userProfile?.id || null, sort_order: notes.length,
      });
      setDraft('');
      await reload();
    } catch (e) { setError(e.message || String(e)); }
    finally { setSaving(false); }
  };

  const addPhoto = async (file) => {
    if (!file) return;
    setPhotoBusy(true); setError(null);
    try {
      const up = await uploadPhotoBlob(file, projektId, g.id, session, workerUrl);
      await insertNote({
        project_id: projektId, gutachten_id: g.id,
        type: 'photo', image_url: up.storage_path, text: '',
        beweisfrage_id: null, user_id: userProfile?.id || null, sort_order: notes.length,
      });
      await reload();
    } catch (e) { setError(e.message || String(e)); }
    finally { setPhotoBusy(false); if (photoInputRef.current) photoInputRef.current.value = ''; }
  };

  // ── Zuordnen / Bearbeiten ─────────────────────────────────────────
  const assign = async (noteId, frageId) => {
    setError(null);
    try { await updateNote(noteId, { beweisfrage_id: frageId || null }); await reload(); }
    catch (e) { setError(e.message || String(e)); }
  };
  const onEdit = async (id, patch) => { await updateNote(id, patch); await reload(); };
  const onDelete = async (id) => {
    if (!window.confirm('Notiz löschen?')) return;
    try { await deleteNote(id); await reload(); } catch (e) { setError(e.message || String(e)); }
  };

  // ── Gruppierung / Abdeckung ───────────────────────────────────────
  const unassigned = notes.filter(n => !n.beweisfrage_id);
  const notesOf = (fid) => notes.filter(n => n.beweisfrage_id === fid);
  const fragenMitBefund = fragen.filter(f => notesOf(f.id).length > 0).length;
  const kurz = (s, n = 90) => { const t = (s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n) + '…' : t; };

  // Zuordnungs-Dropdown (eine Notiz → Frage)
  const Zuordnung = ({ note }) => (
    <select
      value={note.beweisfrage_id || ''}
      onChange={(e) => assign(note.id, e.target.value)}
      style={{
        fontSize: 12, padding: '4px 8px', borderRadius: 'var(--radius-sm)',
        border: '1px solid var(--border-light)', background: 'var(--surface)',
        color: 'var(--text-secondary)', maxWidth: 280,
      }}
      title="Dieser Beweisfrage zuordnen"
    >
      <option value="">— nicht zugeordnet —</option>
      {fragen.map(f => (
        <option key={f.id} value={f.id}>
          {f.nummer || '—'}{f.behauptung ? `  ·  ${kurz(f.behauptung, 50)}` : ''}
        </option>
      ))}
    </select>
  );

  const NoteWithAssign = ({ note }) => (
    <div style={{ marginBottom: 'var(--space-2)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
        <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Zugeordnet:</span>
        <Zuordnung note={note} />
      </div>
      <NoteCard note={note} session={session} workerUrl={workerUrl}
        onEdit={onEdit} onDelete={onDelete} objekte={g.objekte || []} />
    </div>
  );

  return (
    <div>
      {/* Abdeckungs-Hero */}
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap',
        gap: 'var(--space-3)', marginBottom: 'var(--space-4, 20px)',
      }}>
        <div>
          <div style={{ fontSize: 18, fontWeight: 650, color: 'var(--text-primary)' }}>Ortstermin</div>
          <div style={{ fontSize: 13, color: 'var(--text-tertiary)', marginTop: 2 }}>
            {fragenMitBefund} von {fragen.length} Fragen mit Befund
            {unassigned.length > 0 && <> · {unassigned.length} Notiz{unassigned.length === 1 ? '' : 'en'} noch nicht zugeordnet</>}
          </div>
        </div>
      </div>

      {/* Erfassen */}
      <div style={{
        background: 'var(--surface)', border: '1px solid var(--border-light)',
        borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-subtle)',
        padding: 'var(--space-3)', marginBottom: 'var(--space-4, 20px)',
      }}>
        <textarea
          value={draft} onChange={(e) => setDraft(e.target.value)}
          placeholder={'Befund notieren… (z.B. „Silikonfuge Eingangsbereich ungleichmäßig, Breite ca. 5 mm")'}
          style={{
            width: '100%', minHeight: 64, padding: 'var(--space-2)', resize: 'vertical',
            border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
            fontFamily: 'inherit', fontSize: 14, lineHeight: 1.5,
          }}
          onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); addTextNote(); } }}
        />
        <div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-2)', alignItems: 'center', flexWrap: 'wrap' }}>
          <button type="button" className="btn btn-primary btn-sm" onClick={addTextNote} disabled={saving || !draft.trim()}>
            {saving ? 'Speichert…' : 'Notiz speichern'}
          </button>
          <input ref={photoInputRef} type="file" accept="image/*" capture="environment"
            style={{ display: 'none' }} onChange={(e) => addPhoto(e.target.files?.[0])} />
          <button type="button" className="btn btn-secondary btn-sm" onClick={() => photoInputRef.current?.click()} disabled={photoBusy}>
            {photoBusy ? 'Lädt Foto…' : '📷 Foto'}
          </button>
          <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>⌘/Strg+Enter speichert</span>
        </div>
        {error && <div style={{ color: 'var(--danger, #c0392b)', fontSize: 13, marginTop: 8 }}>{error}</div>}
      </div>

      {loading && <div style={{ color: 'var(--text-tertiary)', fontSize: 13, marginBottom: 12 }}>Lädt Notizen…</div>}

      {/* Nicht zugeordnet */}
      {unassigned.length > 0 && (
        <div style={{ marginBottom: 'var(--space-4, 20px)' }}>
          <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--warning)', marginBottom: 'var(--space-2)' }}>
            Noch nicht zugeordnet ({unassigned.length})
          </div>
          {unassigned.map(n => <NoteWithAssign key={n.id} note={n} />)}
        </div>
      )}

      {/* Pro Beweisfrage */}
      {fragen.map(f => {
        const fn = notesOf(f.id);
        return (
          <div key={f.id} style={{
            marginBottom: 'var(--space-3)', background: 'var(--surface)',
            border: '1px solid var(--border-light)', borderRadius: 'var(--radius-lg)',
            boxShadow: 'var(--shadow-subtle)', overflow: 'hidden',
          }}>
            <div style={{
              padding: '11px 14px', borderBottom: fn.length ? '1px solid var(--border-light)' : 'none',
              background: 'var(--surface-raised)',
            }}>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
                <span style={{ fontWeight: 650, fontSize: 14, color: 'var(--text-primary)' }}>{f.nummer || '—'}</span>
                <span style={{
                  fontSize: 11, fontWeight: 600, padding: '1px 8px', borderRadius: 999,
                  background: fn.length ? 'var(--success-bg)' : 'var(--surface-light)',
                  color: fn.length ? 'var(--success)' : 'var(--text-tertiary)',
                }}>
                  {fn.length ? `${fn.length} Befund${fn.length === 1 ? '' : 'e'}` : 'offen'}
                </span>
              </div>
              {f.behauptung && (
                <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 3, lineHeight: 1.45 }}>
                  {kurz(f.behauptung, 160)}
                </div>
              )}
            </div>
            {fn.length > 0 && (
              <div style={{ padding: '12px 14px' }}>
                {fn.map(n => <NoteWithAssign key={n.id} note={n} />)}
              </div>
            )}
          </div>
        );
      })}

      {fragen.length === 0 && !loading && (
        <div style={{ padding: 'var(--space-4)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>
          Keine zuständigen Beweisfragen. Lege sie erst im Tab „Beweisfragen" an.
        </div>
      )}
    </div>
  );
};

// ─────────────────────────────────────────────────────────────────
// INTEGRATION — GutachtenView (vl-app.jsx ~Z. 17304)
// ─────────────────────────────────────────────────────────────────
// Den Ortstermin-Block auf g.art verzweigen (gleiches Muster wie stammdaten):
//
//   {gutachtenTab === 'ortstermin' && (
//     g.art === 'bauschaden' ? (
//       <BeweisfragenOrtsterminTab
//         p={p} g={g} session={session} workerUrl={workerUrl}
//         userProfile={userProfile} onRefresh={onRefresh} />
//     ) : (
//       <OrtsterminTab
//         p={p} g={g} aktiverGutachtenIdx={aktiverGutachtenIdx}
//         session={session} workerUrl={workerUrl} userProfile={userProfile}
//         onRefresh={onRefresh} />
//     )
//   )}
//
// Voraussetzung: Migration 08 (notes.beweisfrage_id) eingespielt; g.beweisfragen
// wird bereits geladen (Phase-1-Integrationspunkt 4).
//
// Hinweis Konsistenz: React-Hooks im Modul werden bare verwendet (useState,
// useMemo, …). Falls in 04_BeweisfragenTab.jsx `React.useState` steht, dort
// ebenfalls auf bare Hooks umstellen (oder sicherstellen, dass `React` im
// Scope ist) — passend zum restlichen Code.

// ════════════════════════════════════════════════════════════════════
// BeweisfragenEntwurfTab — Entwurf für art='bauschaden'
// ════════════════════════════════════════════════════════════════════
// Zeigt pro Beweisfrage Feststellungen / Stellungnahme / Ergebnis. Generiert
// gesammelt oder je Frage über die SSE-Endpoints (10/12), streamt den
// Fortschritt live in die Karten und macht alle Texte inline editierbar.
//
// Reuse: EditableField (type='textarea'), apiPatchRow('beweisfragen', …),
// Pill, das SSE-Lese-Muster des bestehenden EntwurfTab. Texte liegen in den
// beweisfragen-Spalten (feststellungen/stellungnahme/ergebnis + Verdikt-
// Felder) — kein gutachten_tag_outputs nötig. Verzweigung über g.art (unten).
//
// React-Hooks bare (useState/…), wie im Modul üblich.
// ════════════════════════════════════════════════════════════════════

const ZUTREFFEND_OPTIONS = [
  { value: '', label: '—' },
  { value: 'zutreffend', label: 'zutreffend' },
  { value: 'unzutreffend', label: 'unzutreffend' },
  { value: 'teilweise', label: 'teilweise' },
];

const BeweisfragenEntwurfTab = ({ g, p, session, workerUrl, onRefresh }) => {
  // Lokaler Stand, seed aus g.beweisfragen, live durch SSE aktualisiert
  const seed = useMemo(
    () => [...(g.beweisfragen || [])]
      .filter(f => f.zustaendig !== false)
      .sort((a, b) => (a.sortier_index ?? 0) - (b.sortier_index ?? 0)),
    [g.beweisfragen]
  );
  const [fragen, setFragen] = useState(seed);
  useEffect(() => { setFragen(seed); }, [seed]);

  const [busy, setBusy] = useState(false);            // 'feststellungen' | 'wuerdigung' | false
  const busyRef = useRef(false);
  const [activeFrage, setActiveFrage] = useState(null); // Einzel-Generierung: welche Frage
  const [progress, setProgress] = useState({ phase: '', message: '', i: 0, total: 0, nummer: '' });
  const [error, setError] = useState(null);
  const [exporting, setExporting] = useState(false);

  // ── Inline-Edit (optimistisch + persistieren) ──
  const patch = async (id, field, value) => {
    setFragen(prev => prev.map(f => f.id === id ? { ...f, [field]: value } : f));
    try { await apiPatchRow('beweisfragen', id, { [field]: value }, session, workerUrl); }
    catch (e) { setError(`Speichern fehlgeschlagen: ${e.message || e}`); }
  };
  // Mangel-Toggle (true/false/null) als Spaltenwert
  const patchMangel = (id, v) => patch(id, 'mangel_festgestellt', v === '' ? null : v === 'ja');

  // ── SSE-Event → lokalen Stand füllen ──
  const applyEvent = (ev) => {
    if (ev.type === 'phase' || ev.type === 'progress') {
      setProgress(pr => ({
        ...pr,
        phase: ev.phase || pr.phase,
        message: ev.message || '',
        i: ev.i ?? pr.i, total: ev.total ?? pr.total, nummer: ev.nummer ?? pr.nummer,
      }));
    } else if (ev.type === 'frage_done') {
      setFragen(prev => prev.map(f => f.id === ev.id ? {
        ...f,
        ...(ev.feststellungen !== undefined ? { feststellungen: ev.feststellungen } : {}),
        ...(ev.stellungnahme !== undefined ? { stellungnahme: ev.stellungnahme } : {}),
        ...(ev.ergebnis !== undefined ? { ergebnis: ev.ergebnis } : {}),
        ...(ev.mangel !== undefined ? { mangel_festgestellt: ev.mangel } : {}),
        ...(ev.zutreffend !== undefined ? { behauptung_zutreffend: ev.zutreffend } : {}),
        status: ev.stellungnahme !== undefined ? 'fertig'
              : ev.feststellungen !== undefined ? 'feststellung' : f.status,
      } : f));
    } else if (ev.type === 'error') {
      setError(ev.error || 'Generierung fehlgeschlagen');
    }
  };

  // ── SSE-Runner (Muster aus EntwurfTab) ──
  const runSSE = async (endpoint, body, label, frageId = null) => {
    if (busyRef.current) return;
    busyRef.current = true; setBusy(label); setActiveFrage(frageId); setError(null);
    setProgress({ phase: 'starting', message: 'Vorbereitung…', i: 0, total: 0, nummer: '' });
    try {
      const res = await fetch(`${workerUrl}${endpoint}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
        body: JSON.stringify(body),
      });
      if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || `HTTP ${res.status}`); }
      const reader = res.body.getReader();
      const dec = new TextDecoder();
      let buf = '';
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buf += dec.decode(value, { stream: true });
        const lines = buf.split('\n');
        buf = lines.pop() || '';
        for (const line of lines) {
          if (line.startsWith(':')) continue;          // Heartbeat
          if (!line.startsWith('data: ')) continue;
          let ev; try { ev = JSON.parse(line.slice(6)); } catch { continue; }
          applyEvent(ev);
        }
      }
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      busyRef.current = false; setBusy(false); setActiveFrage(null);
      setProgress(pr => ({ ...pr, phase: 'complete', message: '' }));
      try { onRefresh && onRefresh(); } catch {}
    }
  };

  const FESTST = '/api/entwurf/bauschaden-feststellungen';
  const WUERD  = '/api/entwurf/bauschaden-wuerdigung';
  const genFeststAll  = () => runSSE(FESTST, { gutachten_id: g.id }, 'feststellungen');
  const genWuerdAll   = () => runSSE(WUERD,  { gutachten_id: g.id }, 'wuerdigung');
  const genFeststOne  = (id) => runSSE(FESTST, { gutachten_id: g.id, beweisfrage_id: id }, 'feststellungen', id);
  const genWuerdOne   = (id) => runSSE(WUERD,  { gutachten_id: g.id, beweisfrage_id: id }, 'wuerdigung', id);

  // ── DOCX-Export (Phase 5) ──
  const exportDocx = async () => {
    setExporting(true); setError(null);
    try {
      const res = await fetch(`${workerUrl}/api/entwurf/render-docx-bauschaden`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
        body: JSON.stringify({ gutachten_id: g.id }),
      });
      if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || e.detail || `HTTP ${res.status}`); }
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = res.headers.get('Content-Disposition')?.match(/filename="?([^"]+)"?/)?.[1] || `Gutachten_${g.id}.docx`;
      document.body.appendChild(link); link.click(); document.body.removeChild(link);
      URL.revokeObjectURL(url);
    } catch (e) { setError(`DOCX-Export fehlgeschlagen: ${e.message || e}`); }
    finally { setExporting(false); }
  };

  // ── Kennzahlen ──
  const hasFest = (f) => f.feststellungen && f.feststellungen.trim() && !f.feststellungen.startsWith('[ERGÄNZEN');
  const mitFest = fragen.filter(hasFest).length;
  const fertig  = fragen.filter(f => f.status === 'fertig').length;

  const sektionLabel = { feststellungen: 'Feststellungen', stellungnahme: 'Stellungnahme', ergebnis: 'Ergebnis' };

  // Editierbarer Textabschnitt mit „neu generieren"-Knopf
  const TextSektion = ({ frage, feld, onRegen, regenDisabled, regenLabel }) => (
    <div style={{ marginTop: 'var(--space-3)' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
        <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-secondary)', letterSpacing: 0.2 }}>
          {sektionLabel[feld]}
        </span>
        {onRegen && (
          <button type="button" onClick={onRegen} disabled={regenDisabled}
            style={{ background: 'none', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
                     padding: '2px 8px', fontSize: 11, color: 'var(--vl-blue)', cursor: regenDisabled ? 'default' : 'pointer', opacity: regenDisabled ? 0.5 : 1 }}>
            {regenLabel || '↻ neu'}
          </button>
        )}
      </div>
      <EditableField
        value={frage[feld] || ''}
        type="textarea"
        placeholder={`${sektionLabel[feld]} – noch nicht erstellt`}
        onSave={(v) => patch(frage.id, feld, v)}
      />
    </div>
  );

  return (
    <div>
      {/* Kopf + Aktionen */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 'var(--space-3)', marginBottom: 'var(--space-4, 20px)' }}>
        <div>
          <div style={{ fontSize: 18, fontWeight: 650, color: 'var(--text-primary)' }}>Entwurf</div>
          <div style={{ fontSize: 13, color: 'var(--text-tertiary)', marginTop: 2 }}>
            {mitFest} von {fragen.length} mit Feststellung · {fertig} fertig (mit Ergebnis)
          </div>
        </div>
        <div style={{ display: 'flex', gap: 'var(--space-2)', flexWrap: 'wrap' }}>
          <button type="button" className="btn btn-secondary btn-sm" onClick={genFeststAll} disabled={!!busy}>
            {busy === 'feststellungen' && !activeFrage ? 'Generiert…' : 'Feststellungen generieren'}
          </button>
          <button type="button" className="btn btn-primary btn-sm" onClick={genWuerdAll} disabled={!!busy || mitFest === 0}
            title={mitFest === 0 ? 'Zuerst Feststellungen generieren' : 'Stellungnahme + Ergebnis für alle Fragen'}>
            {busy === 'wuerdigung' && !activeFrage ? 'Generiert…' : 'Stellungnahme + Ergebnis generieren'}
          </button>
          <button type="button" className="btn btn-ghost btn-sm" onClick={exportDocx} disabled={exporting || !!busy}
            title="Gutachten als Word-Dokument exportieren">
            {exporting ? 'Exportiert…' : 'DOCX exportieren'}
          </button>
        </div>
      </div>

      {/* Fortschritt */}
      {busy && (
        <div style={{ background: 'var(--surface-blue, #EEF3F8)', border: '1px solid var(--vl-blue-light, #6B8DB5)',
                      borderRadius: 'var(--radius-md)', padding: '10px 14px', marginBottom: 'var(--space-3)', fontSize: 13, color: 'var(--vl-blue)' }}>
          {progress.message || 'Generiert…'}
          {progress.total ? ` (${(progress.i ?? 0) + 1}/${progress.total}${progress.nummer ? `, ${progress.nummer}` : ''})` : ''}
        </div>
      )}
      {error && (
        <div style={{ color: 'var(--danger, #c0392b)', fontSize: 13, marginBottom: 'var(--space-3)' }}>{error}</div>
      )}

      {/* Karten je Frage */}
      {fragen.map(f => {
        const fertigF = f.status === 'fertig';
        const mangel = f.mangel_festgestellt;
        const busyThis = busy && activeFrage === f.id;
        return (
          <div key={f.id} style={{ marginBottom: 'var(--space-3)', background: 'var(--surface)',
                                    border: '1px solid var(--border-light)', borderRadius: 'var(--radius-lg)',
                                    boxShadow: 'var(--shadow-subtle)', overflow: 'hidden' }}>
            {/* Kopf: Nummer, Verdikt-Badges */}
            <div style={{ padding: '11px 14px', borderBottom: '1px solid var(--border-light)', background: 'var(--surface-raised)' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                <span style={{ fontWeight: 650, fontSize: 14, color: 'var(--text-primary)' }}>{f.nummer || '—'}</span>
                {fertigF && (
                  <Pill variant={mangel === true ? 'red' : mangel === false ? 'green' : 'gray'} style={{ fontSize: 10 }}>
                    {mangel === true ? 'Mangel' : mangel === false ? 'kein Mangel' : 'Ergebnis offen'}
                  </Pill>
                )}
                {f.behauptung_zutreffend && (
                  <Pill variant="blue" style={{ fontSize: 10 }}>Behauptung {f.behauptung_zutreffend}</Pill>
                )}
                {busyThis && <span style={{ fontSize: 11, color: 'var(--vl-blue)' }}>generiert…</span>}
              </div>
              {/* Behauptung (editierbar) */}
              <div style={{ marginTop: 6 }}>
                <EditableField value={f.behauptung || ''} type="textarea"
                  placeholder="Behauptung / Beweisfrage" onSave={(v) => patch(f.id, 'behauptung', v)} />
              </div>
            </div>

            {/* Inhalt */}
            <div style={{ padding: '12px 14px' }}>
              <TextSektion frage={f} feld="feststellungen"
                onRegen={() => genFeststOne(f.id)} regenDisabled={!!busy} />

              <TextSektion frage={f} feld="stellungnahme"
                onRegen={() => genWuerdOne(f.id)} regenDisabled={!!busy || !hasFest(f)}
                regenLabel="↻ Stellungnahme + Ergebnis" />

              <TextSektion frage={f} feld="ergebnis" />

              {/* Verdikt manuell korrigierbar */}
              <div style={{ display: 'flex', gap: 'var(--space-3)', alignItems: 'center', marginTop: 'var(--space-2)', flexWrap: 'wrap' }}>
                <label style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Behauptung:</label>
                <EditableField value={f.behauptung_zutreffend || ''} type="select" options={ZUTREFFEND_OPTIONS}
                  display={f.behauptung_zutreffend || '—'} onSave={(v) => patch(f.id, 'behauptung_zutreffend', v || null)} />
                <label style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Mangel:</label>
                <EditableField
                  value={mangel === true ? 'ja' : mangel === false ? 'nein' : ''}
                  type="select"
                  options={[{ value: '', label: '—' }, { value: 'ja', label: 'ja' }, { value: 'nein', label: 'nein' }]}
                  display={mangel === true ? 'ja' : mangel === false ? 'nein' : '—'}
                  onSave={(v) => patchMangel(f.id, v)} />
              </div>
            </div>
          </div>
        );
      })}

      {fragen.length === 0 && (
        <div style={{ padding: 'var(--space-4)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>
          Keine zuständigen Beweisfragen. Lege sie erst im Tab „Beweisfragen" an.
        </div>
      )}
    </div>
  );
};

// ─────────────────────────────────────────────────────────────────
// INTEGRATION — GutachtenView (vl-app.jsx ~Z. 17315, Entwurf-Block)
// ─────────────────────────────────────────────────────────────────
//   {gutachtenTab === 'entwurf' && (
//     g.art === 'bauschaden' ? (
//       <BeweisfragenEntwurfTab g={g} p={p} session={session}
//         workerUrl={workerUrl} onRefresh={onRefresh} />
//     ) : (
//       <EntwurfTab g={g} p={p} session={session} workerUrl={workerUrl} onRefresh={onRefresh} />
//     )
//   )}
//
// Voraussetzungen:
//   - g.beweisfragen wird geladen (Phase-1-Integrationspunkt 4)
//   - 'beweisfragen' steht in der /api/row-Audit-Allowlist (Phase-1-Punkt 5),
//     damit apiPatchRow die Inline-Edits schreiben darf
//   - Endpoints aus 10/12 sind im Worker-Router registriert

// ════════════════════════════════════════════════════════════════════
// BeweisfragenAbgabe — Abgabe-Bereich für art='bauschaden' (Phase 6)
// ════════════════════════════════════════════════════════════════════
// Letzter Lebenszyklus-Schritt. Nutzt das BESTEHENDE, gewerksneutrale
// Statusmodell (auftraege.bearbeitungsstatus, 6 Phasen) — kein neues Modell,
// keine harte Sperre (wie im VWG-Pfad ist „abgeschlossen" ein Statuslabel).
//
// Zeigt: Bereitschaftsprüfung (alle zuständigen Fragen fertig), DOCX-Export,
// Statuswechsel auf „abgeschlossen", Verteiler-Hinweis.
//
// Reuse: apiPatchRow('auftraege', p.auftrag_id, …), AUFTRAG_STATUS_OPTIONS/
// _BY_VALUE, Pill, der Render-Endpoint aus 14. Bare React-Hooks wie im Modul.
// ════════════════════════════════════════════════════════════════════

const BeweisfragenAbgabe = ({ g, p, session, workerUrl, onRefresh }) => {
  const fragen = useMemo(
    () => (g.beweisfragen || []).filter(f => f.zustaendig !== false),
    [g.beweisfragen]
  );
  const fertig = fragen.filter(f => f.status === 'fertig').length;
  const offen = fragen.length - fertig;
  const ready = fragen.length > 0 && offen === 0;

  const statusInfo = (typeof AUFTRAG_STATUS_BY_VALUE !== 'undefined')
    ? AUFTRAG_STATUS_BY_VALUE[p.bearbeitungsstatus || 'angelegt'] : null;
  const abgeschlossen = (p.bearbeitungsstatus === 'abgeschlossen');

  const [busy, setBusy] = useState(null); // 'export' | 'abgeben' | null
  const [error, setError] = useState(null);

  const exportDocx = async () => {
    setBusy('export'); setError(null);
    try {
      const res = await fetch(`${workerUrl}/api/entwurf/render-docx-bauschaden`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
        body: JSON.stringify({ gutachten_id: g.id }),
      });
      if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || e.detail || `HTTP ${res.status}`); }
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = res.headers.get('Content-Disposition')?.match(/filename="?([^"]+)"?/)?.[1] || `Gutachten_${g.id}.docx`;
      document.body.appendChild(link); link.click(); document.body.removeChild(link);
      URL.revokeObjectURL(url);
    } catch (e) { setError(`DOCX-Export fehlgeschlagen: ${e.message || e}`); }
    finally { setBusy(null); }
  };

  const abgeben = async () => {
    if (!p.auftrag_id) { setError('Auftrag noch nicht angelegt.'); return; }
    if (!window.confirm('Gutachten als abgeschlossen markieren? Der Status wird auf „06 Auftrag abgeschlossen" gesetzt.')) return;
    setBusy('abgeben'); setError(null);
    try {
      await apiPatchRow('auftraege', p.auftrag_id, { bearbeitungsstatus: 'abgeschlossen' }, session, workerUrl);
      try { onRefresh && onRefresh(); } catch {}
    } catch (e) { setError(`Statuswechsel fehlgeschlagen: ${e.message || e}`); }
    finally { setBusy(null); }
  };

  const Check = ({ ok, children }) => (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--text-secondary)', padding: '3px 0' }}>
      <span style={{
        width: 18, height: 18, borderRadius: 999, flexShrink: 0, display: 'inline-flex',
        alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700,
        background: ok ? 'var(--success-bg)' : 'var(--surface-light)',
        color: ok ? 'var(--success)' : 'var(--text-tertiary)',
        border: `1px solid ${ok ? 'var(--success)' : 'var(--border-light)'}`,
      }}>{ok ? '✓' : '○'}</span>
      <span>{children}</span>
    </div>
  );

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
        <div style={{ fontSize: 18, fontWeight: 650, color: 'var(--text-primary)' }}>Abgabe</div>
        {statusInfo && (
          <span className="pill" style={{ color: statusInfo.fg, background: statusInfo.bg, whiteSpace: 'nowrap' }}>
            {statusInfo.label}
          </span>
        )}
      </div>

      {/* Bereitschaft */}
      <div style={{ background: 'var(--surface)', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-subtle)', padding: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
        <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-secondary)', marginBottom: 6 }}>Bereitschaft</div>
        <Check ok={fragen.length > 0}>Beweisfragen vorhanden ({fragen.length})</Check>
        <Check ok={ready}>
          Alle Fragen fertig bearbeitet ({fertig}/{fragen.length}){offen > 0 ? ` — ${offen} offen` : ''}
        </Check>
        {!ready && fragen.length > 0 && (
          <div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 6 }}>
            Offene Fragen: erst im Tab „Entwurf" Stellungnahme + Ergebnis erzeugen.
          </div>
        )}
      </div>

      {/* Aktionen */}
      <div style={{ display: 'flex', gap: 'var(--space-2)', flexWrap: 'wrap', marginBottom: 'var(--space-3)' }}>
        <button type="button" className="btn btn-secondary btn-sm" onClick={exportDocx} disabled={busy === 'export'}>
          {busy === 'export' ? 'Exportiert…' : 'DOCX exportieren'}
        </button>
        {!abgeschlossen ? (
          <button type="button" className="btn btn-primary btn-sm" onClick={abgeben} disabled={busy === 'abgeben'}
            title={ready ? 'Status auf abgeschlossen setzen' : 'Es sind noch nicht alle Fragen fertig — Abgabe trotzdem möglich'}>
            {busy === 'abgeben' ? 'Wird gesetzt…' : 'Gutachten abgeben'}
          </button>
        ) : (
          <span style={{ alignSelf: 'center', fontSize: 13, color: 'var(--success)', fontWeight: 600 }}>
            ✓ Abgeschlossen
          </span>
        )}
      </div>
      {error && <div style={{ color: 'var(--danger, #c0392b)', fontSize: 13, marginBottom: 'var(--space-3)' }}>{error}</div>}

      {/* Verteiler-Hinweis */}
      <div style={{ fontSize: 12, color: 'var(--text-tertiary)', lineHeight: 1.5, borderTop: '1px solid var(--border-light)', paddingTop: 'var(--space-2)' }}>
        Verteiler: PDF-Ausfertigung an das Gericht (Auftraggeber), eine Ausfertigung für die eigenen Unterlagen.
        Der Status erscheint auch im Auftrags-Stepper; „abgeschlossen" sperrt die Bearbeitung nicht hart.
      </div>
    </div>
  );
};

// ─────────────────────────────────────────────────────────────────
// INTEGRATION — GutachtenView (finaler Tab, ~Z. 17315 ff.)
// ─────────────────────────────────────────────────────────────────
// Auf dem Abschluss-Tab für Bauschäden mounten (VWG-Endtab heißt 'gutachten'):
//
//   {gutachtenTab === 'gutachten' && g.art === 'bauschaden' && (
//     <BeweisfragenAbgabe g={g} p={p} session={session}
//       workerUrl={workerUrl} onRefresh={onRefresh} />
//   )}
//
// Alternativ als Abschluss-Block unten in BeweisfragenEntwurfTab (13) einhängen
// — dann finalisiert der SV Generieren → Export → Abgabe an einer Stelle.
//
// Voraussetzungen: p.auftrag_id vorhanden; AUFTRAG_STATUS_OPTIONS/_BY_VALUE,
// apiPatchRow('auftraege', …) und der Render-Endpoint (14) im Scope/aktiv.

const GutachtenView = ({
  p, aktiverGutachtenIdx, aktivesObjekt, gutachtenTab,
  onSwitchGutachten, onSwitchObjekt, onSwitchTab,
  onOpenAuftrag, onOpenUpload, onOpenBulkUpload, onDeleteGutachten,
  herkunft, onOpenDocument,
  session, workerUrl, userProfile,
  onRefresh,
}) => {
  const g = p.gutachten[aktiverGutachtenIdx];

  // ── Gutachtenweite Befehls-Palette (⌘K / Strg-K): Reiter, Objekt, Feld ──
  const [paletteOpen, setPaletteOpen] = useState(false);
  const [pendingFeld, setPendingFeld] = useState(null);
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
        e.preventDefault(); setPaletteOpen(o => !o);
      } else if (e.key === 'Escape' && paletteOpen) {
        setPaletteOpen(false);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [paletteOpen]);
  const paletteItems = useMemo(() => {
    const TABS = [
      { id: 'dashboard', label: 'Dashboard' }, { id: 'auftrag', label: 'Auftrag' },
      { id: 'unterlagen', label: 'Unterlagen' }, { id: 'stammdaten', label: 'Objekte' },
      { id: 'ortstermin', label: 'Ortstermin' }, { id: 'fotos', label: 'Fotos' },
      { id: 'entwurf', label: 'Entwurf' }, { id: 'gutachten', label: 'Gutachten' },
    ];
    const objekte = (g && g.objekte) || [];
    const aIdx = objekte.length ? Math.max(0, Math.min(aktivesObjekt, objekte.length - 1)) : 0;
    const objX = objekte[aIdx] || null;
    const feldWert = (key) => key === 'adresse' ? (g && g.adresse) : (objX ? objX[key] : null);
    const feldItems = JUMP_FELD_ORDER.map(key => {
      const w = feldWert(key);
      return {
        id: 'feld:' + key, group: 'Felder', kind: 'feld',
        label: FELD_LABEL[key] || key, sub: FELD_SEKTION_MAP[key] || '',
        filled: !(w == null || w === ''), keywords: key,
        run: () => { onSwitchTab('stammdaten'); setPendingFeld(key); },
      };
    });
    const objItems = objekte.map((o, i) => ({
      id: 'obj:' + i, group: 'Objekte', kind: 'objekt',
      label: o.bezeichnung || ('Objekt ' + (i + 1)),
      sub: i === aIdx ? 'aktuell' : 'wechseln', current: i === aIdx,
      keywords: 'objekt ' + (i + 1),
      run: () => { onSwitchTab('stammdaten'); onSwitchObjekt(i); window.scrollTo({ top: 0, behavior: 'smooth' }); },
    }));
    const tabItems = TABS.map(t => ({
      id: 'tab:' + t.id, group: 'Reiter', kind: 'objekt',
      label: t.label, sub: t.id === gutachtenTab ? 'aktuell' : 'öffnen', current: t.id === gutachtenTab,
      keywords: t.label, run: () => onSwitchTab(t.id),
    }));
    return [...feldItems, ...objItems, ...tabItems];
  }, [g, aktivesObjekt, gutachtenTab, onSwitchTab, onSwitchObjekt]);

  // Defensiv: Projekt ohne Gutachten (sollte durch createProjekt nie passieren,
  // aber Mobile springt jetzt direkt hierher — kein Crash bei Altdaten).
  if (!g) {
    return (
      <div className="view-wrapper">
        <div style={{ padding: 'var(--space-5)', textAlign: 'center', color: 'var(--text-tertiary)' }}>
          Für dieses Projekt ist noch kein Gutachten angelegt.
        </div>
      </div>
    );
  }

  return (
    <div className="view-wrapper">
      <GutachtenToolbar
        p={p} g={g} aktiverIdx={aktiverGutachtenIdx}
        gutachtenTab={gutachtenTab}
        onSwitchGutachten={onSwitchGutachten}
        onOpenAuftrag={onOpenAuftrag}
        onDeleteGutachten={onDeleteGutachten}
        session={session} workerUrl={workerUrl} onRefresh={onRefresh}
      />
      <CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} items={paletteItems} />
      <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 'var(--space-3)' }}>
        <button type="button" onClick={() => setPaletteOpen(true)} className="cmdk-trigger" title="Schnell springen — Reiter, Objekt oder Feld (⌘K / Strg-K)">
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></svg>
          Springen zu…
          <span className="cmdk-trigger-kbd">⌘K</span>
        </button>
      </div>
      {/* GutachtenTabs entfernt — Navigation über Projekt-Tab-Bar */}
      <div key={gutachtenTab} className="view-fade">
        {gutachtenTab === 'stammdaten' && (g.art === 'bauschaden' ? (
          <BeweisfragenTab g={g} p={p} session={session} workerUrl={workerUrl} onRefresh={onRefresh} />
        ) : (
          <StammdatenTab
            g={g}
            aktivesObjekt={aktivesObjekt}
            onSwitchObjekt={onSwitchObjekt}
            herkunft={herkunft}
            dokumente={p.dokumente}
            session={session}
            workerUrl={workerUrl}
            onRefresh={onRefresh}
            pendingFeld={pendingFeld}
            onFeldConsumed={() => setPendingFeld(null)}
          />
        ))}
        {gutachtenTab === 'unterlagen' && (
          <UnterlagenTab
            p={p}
            g={g}
            session={session}
            workerUrl={workerUrl}
            onRefresh={onRefresh}
            onOpenUpload={onOpenUpload}
            onOpenBulkUpload={onOpenBulkUpload}
          />
        )}
        {gutachtenTab === 'ortstermin' && (g.art === 'bauschaden' ? (
          <BeweisfragenOrtsterminTab p={p} g={g} session={session} workerUrl={workerUrl} userProfile={userProfile} onRefresh={onRefresh} />
        ) : (
          <OrtsterminTab
            p={p}
            g={g}
            aktiverGutachtenIdx={aktiverGutachtenIdx}
            session={session}
            workerUrl={workerUrl}
            userProfile={userProfile}
            onRefresh={onRefresh}
          />
        ))}
        {gutachtenTab === 'entwurf' && (g.art === 'bauschaden' ? (
          <BeweisfragenEntwurfTab g={g} p={p} session={session} workerUrl={workerUrl} onRefresh={onRefresh} />
        ) : (
          <EntwurfTab
            g={g}
            p={p}
            session={session}
            workerUrl={workerUrl}
            onRefresh={onRefresh}
          />
        ))}
        {gutachtenTab === 'fotos' && (
          <>
            <FotosGrid gutachtenId={g.id} session={session} projektId={p.id} workerUrl={workerUrl} />
            <FotoZuordnung gutachten={g} session={session} workerUrl={workerUrl} projektId={p.id} onRefresh={onRefresh} />
          </>
        )}
        {gutachtenTab === 'gutachten' && (g.art === 'bauschaden' ? (
          <BeweisfragenAbgabe g={g} p={p} session={session} workerUrl={workerUrl} onRefresh={onRefresh} />
        ) : (
          <GutachtenExportTab g={g} p={p} session={session} workerUrl={workerUrl} />
        ))}
      </div>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════
// GUTACHTEN EXPORT TAB
// ══════════════════════════════════════════════════════════════════

const GutachtenExportTab = ({ g, p, session, workerUrl }) => {
  const [exporting, setExporting] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  const fotoZuordnung = g.foto_zuordnung || {};
  const zugeordnet = FOTO_SLOTS.filter(s => fotoZuordnung[s.id]);

  const exportDocx = async () => {
    setExporting(true);
    setError(null);
    setSuccess(false);
    try {
      let token = session.access_token;
      try { const sb = await initSupabase(); const { data } = await sb.auth.getSession(); if (data?.session?.access_token) token = data.session.access_token; } catch {}
      const res = await fetch(`${workerUrl}/api/entwurf/render-docx`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
        body: JSON.stringify({ gutachten_id: g.id }),
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error(err.error || err.detail || `HTTP ${res.status}`);
      }
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = res.headers.get('Content-Disposition')?.match(/filename="?([^"]+)"?/)?.[1] || `Gutachten_${g.id}.docx`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      setSuccess(true);
    } catch (e) {
      console.error('[Gutachten] DOCX export failed:', e);
      setError(e.message);
    } finally {
      setExporting(false);
    }
  };

  const obj = p.gutachten?.[0]?.objekte?.[0] || {};
  const auftrag = p;

  return (
    <div style={{ maxWidth: 700 }}>
      <div className="card" style={{ marginBottom: 'var(--space-4)' }}>
        <div className="card-header">
          <span className="card-title">Gutachten exportieren</span>
        </div>
        <div style={{ padding: 'var(--space-4)' }}>
          <table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
            <tbody>
              {[
                ['Adresse', g.adresse],
                ['Aktenzeichen', auftrag.aktenzeichen],
                ['Stichtag', g.wertermittlungsstichtag ? new Date(g.wertermittlungsstichtag).toLocaleDateString('de-DE') : g.ortstermin_datum ? new Date(g.ortstermin_datum).toLocaleDateString('de-DE') : null],
                ['Fotos zugeordnet', `${zugeordnet.length} von ${FOTO_SLOTS.length}`],
              ].map(([lbl, val], i) => (
                <tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
                  <td style={{ padding: '8px 12px 8px 0', color: 'var(--text-secondary)', width: '40%' }}>{lbl}</td>
                  <td style={{ padding: '8px 0', fontWeight: val ? 500 : 400, color: val ? 'var(--text-primary)' : 'var(--text-tertiary)' }}>
                    {val || 'Nicht gesetzt'}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>

          {zugeordnet.length < FOTO_SLOTS.length && (
            <div style={{
              marginTop: 'var(--space-3)', padding: '10px 14px',
              background: '#FEF3C7', borderRadius: 'var(--radius-sm)', fontSize: 13, color: '#92400E',
            }}>
              {FOTO_SLOTS.length - zugeordnet.length} Foto-Platzhalter sind noch nicht zugeordnet.
              Fehlende Platzhalter bleiben im DOCX als Texthinweis stehen.
            </div>
          )}

          <div style={{ marginTop: 'var(--space-4)', display: 'flex', gap: 12, alignItems: 'center' }}>
            <button className="btn btn-accent" onClick={exportDocx} disabled={exporting}
              style={{ fontSize: 15, padding: '12px 28px' }}>
              {exporting ? 'Exportiere...' : 'DOCX exportieren'}
            </button>
            {success && (
              <span style={{ color: 'var(--success)', fontSize: 13 }}>Download gestartet</span>
            )}
          </div>

          {error && (
            <div style={{
              marginTop: 'var(--space-3)', padding: '10px 14px',
              background: '#FEE2E2', borderRadius: 'var(--radius-sm)', fontSize: 13, color: '#991B1B',
            }}>
              {error}
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════
// UPLOAD MODAL
// ══════════════════════════════════════════════════════════════════

// Geordnete Kategorien für den Unterlagen-Tab (Akkordeon-Reihenfolge)
const UNTERLAGEN_CATEGORIES = [
  'Auftragsunterlagen',
  'Grundbuchunterlagen',
  'Lagepläne und Grundstücksmaße',
  'Umwelteinflüsse, Naturgefahren (Georisiken) und Immissionen',
  'Bauunterlagen',
  'Baurecht und Erschließung',
  'Schornsteinfeger',
  'Altlasten und Denkmalschutz',
  'WEG-Verwaltung, Gebäudeversicherung und Energieausweis',
  'Miet- und Pachtunterlagen',
  'Unterlagen der Parteien',
  'Korrespondenz',
  'Rechnungen',
  'Sonstiges',
];

const UPLOAD_TYPES = [
  { group: 'Auftragsunterlagen', scope: 'auftrag', items: [
    { id: 'gerichtsbeschluss', label: 'Beweisbeschluss', extractable: true, category: 'Auftragsunterlagen' },
    { id: 'anschreiben', label: 'Auftrag / Anschreiben', extractable: true, category: 'Auftragsunterlagen' },
    { id: 'bestellungsurkunde', label: 'Gerichtliche Bestellungsurkunde', extractable: false, category: 'Auftragsunterlagen' },
    { id: 'sonstiger_beschluss', label: 'Sonstiger Beschluss', extractable: false, category: 'Auftragsunterlagen' },
  ]},
  { group: 'Grundbuchunterlagen', scope: 'objekt', items: [
    { id: 'grundbuchauszug', label: 'Grundbuchauszug', extractable: true, category: 'Grundbuchunterlagen' },
    { id: 'kaufvertrag', label: 'Kaufvertrag', extractable: false, category: 'Grundbuchunterlagen' },
    { id: 'teilungserklaerung', label: 'Teilungserklärung', extractable: true, category: 'Grundbuchunterlagen' },
    { id: 'aufteilungsplan', label: 'Aufteilungsplan', extractable: false, category: 'Grundbuchunterlagen' },
    { id: 'bewilligung', label: 'Bewilligungsurkunde', extractable: false, category: 'Grundbuchunterlagen' },
  ]},
  { group: 'Lagepläne und Grundstücksmaße', scope: 'gutachten', items: [
    { id: 'lagekarte', label: 'Lagekarte', extractable: false, category: 'Lagepläne und Grundstücksmaße' },
    { id: 'luftbild', label: 'Luftbild', extractable: false, category: 'Lagepläne und Grundstücksmaße' },
    { id: 'lageplan_vermessung', label: 'Lageplan / Vermessungsriss', extractable: false, category: 'Lagepläne und Grundstücksmaße' },
    { id: 'demografie', label: 'Demografische Indikatoren', extractable: true, category: 'Lagepläne und Grundstücksmaße' },
  ]},
  { group: 'Umwelteinflüsse, Naturgefahren (Georisiken) und Immissionen', scope: 'gutachten', items: [
    { id: 'umweltgutachten', label: 'Umweltgutachten / Immissionsprognose', extractable: false, category: 'Umwelteinflüsse, Naturgefahren (Georisiken) und Immissionen' },
    { id: 'georisiken', label: 'Georisiken / Naturgefahren', extractable: false, category: 'Umwelteinflüsse, Naturgefahren (Georisiken) und Immissionen' },
    { id: 'hochwasser', label: 'Hochwasserkarte / Überschwemmungsgebiet', extractable: false, category: 'Umwelteinflüsse, Naturgefahren (Georisiken) und Immissionen' },
  ]},
  { group: 'Bauunterlagen', scope: 'objekt', items: [
    { id: 'bauplan', label: 'Grundriss / Schnitt / Ansicht', extractable: false, category: 'Bauunterlagen' },
    { id: 'bautechnik', label: 'Bautechnische Berechnungen', extractable: false, category: 'Bauunterlagen' },
    { id: 'baugenehmigung', label: 'Baugenehmigung / Bauabnahme', extractable: false, category: 'Bauunterlagen' },
    { id: 'wohnflaechenberechnung', label: 'Wohnflächenberechnung', extractable: false, category: 'Bauunterlagen' },
    { id: 'umbau_renovierung', label: 'An-/Umbauten / Renovierungen', extractable: false, category: 'Bauunterlagen' },
  ]},
  { group: 'Baurecht und Erschließung', scope: 'gutachten', items: [
    { id: 'bebauungsplan', label: 'Bebauungsplan', extractable: true, category: 'Baurecht und Erschließung' },
    { id: 'flaechennutzungsplan', label: 'Flächennutzungsplan (Auszug)', extractable: false, category: 'Baurecht und Erschließung' },
    { id: 'baurechtsauskunft', label: 'Baurechtliche Auskunft', extractable: true, category: 'Baurecht und Erschließung' },
    { id: 'bodenrichtwert', label: 'Bodenrichtwertkarte', extractable: true, category: 'Baurecht und Erschließung' },
    { id: 'erschliessung_strasse', label: 'Erschließungsnachweis (Straße)', extractable: false, category: 'Baurecht und Erschließung' },
    { id: 'erschliessung_wasser', label: 'Erschließungsnachweis (Wasser)', extractable: false, category: 'Baurecht und Erschließung' },
    { id: 'erschliessung_kanal', label: 'Erschließungsnachweis (Kanal)', extractable: false, category: 'Baurecht und Erschließung' },
    { id: 'kanalueberpruefung', label: 'Kanalüberprüfung', extractable: false, category: 'Baurecht und Erschließung' },
    { id: 'baulasten', label: 'Baulastenauskunft', extractable: false, category: 'Baurecht und Erschließung' },
  ]},
  { group: 'Schornsteinfeger', scope: 'objekt', items: [
    { id: 'feuerstaette', label: 'Feuerstättenbescheid', extractable: false, category: 'Schornsteinfeger' },
    { id: 'messbescheinigung', label: 'Messbescheinigung', extractable: false, category: 'Schornsteinfeger' },
    { id: 'kehr_ueberpruefung', label: 'Kehr- und Überprüfungsbescheid', extractable: false, category: 'Schornsteinfeger' },
  ]},
  { group: 'Altlasten und Denkmalschutz', scope: 'gutachten', items: [
    { id: 'altlasten', label: 'Altlastenauskunft', extractable: true, category: 'Altlasten und Denkmalschutz' },
    { id: 'denkmalschutz', label: 'Denkmalschutz-Auskunft', extractable: false, category: 'Altlasten und Denkmalschutz' },
  ]},
  { group: 'WEG-Verwaltung, Gebäudeversicherung und Energieausweis', scope: 'objekt', items: [
    { id: 'energieausweis', label: 'Energieausweis', extractable: true, category: 'WEG-Verwaltung, Gebäudeversicherung und Energieausweis' },
    { id: 'weg_protokoll', label: 'WEG-Eigentümerversammlung', extractable: false, category: 'WEG-Verwaltung, Gebäudeversicherung und Energieausweis' },
    { id: 'weg_hausgeld', label: 'WEG-Hausgeldabrechnung', extractable: false, category: 'WEG-Verwaltung, Gebäudeversicherung und Energieausweis' },
    { id: 'weg_verwalter', label: 'WEG-Verwalter-Kontakt', extractable: false, category: 'WEG-Verwaltung, Gebäudeversicherung und Energieausweis' },
    { id: 'weg_wirtschaftsplan', label: 'WEG-Wirtschaftsplan', extractable: false, category: 'WEG-Verwaltung, Gebäudeversicherung und Energieausweis' },
    { id: 'gebaeudeversicherung', label: 'Gebäudeversicherung', extractable: false, category: 'WEG-Verwaltung, Gebäudeversicherung und Energieausweis' },
  ]},
  { group: 'Miet- und Pachtunterlagen', scope: 'objekt', items: [
    { id: 'mietvertrag', label: 'Mietvertrag', extractable: false, category: 'Miet- und Pachtunterlagen' },
    { id: 'mieterhoehung', label: 'Mieterhöhung', extractable: false, category: 'Miet- und Pachtunterlagen' },
    { id: 'pachtvertrag', label: 'Pachtvertrag', extractable: false, category: 'Miet- und Pachtunterlagen' },
    { id: 'mietaufstellung', label: 'Mietaufstellung / Nebenkostenabrechnung', extractable: false, category: 'Miet- und Pachtunterlagen' },
  ]},
  { group: 'Unterlagen der Parteien', scope: 'flex', items: [
    { id: 'parteiunterlage', label: 'Unterlage einer Partei', extractable: false, category: 'Unterlagen der Parteien' },
    { id: 'privatgutachten', label: 'Privatgutachten / Gegengutachten', extractable: false, category: 'Unterlagen der Parteien' },
  ]},
  { group: 'Korrespondenz', scope: 'flex', items: [
    { id: 'korrespondenz', label: 'Korrespondenz / Schriftverkehr', extractable: false, category: 'Korrespondenz' },
    { id: 'gerichtspost', label: 'Gerichtspost / Verfügung', extractable: false, category: 'Korrespondenz' },
  ]},
  { group: 'Rechnungen', scope: 'flex', items: [
    { id: 'rechnung', label: 'Rechnung / Honorarrechnung', extractable: false, category: 'Rechnungen' },
    { id: 'kostenvorschuss_rechnung', label: 'Kostenvorschuss-Anforderung', extractable: false, category: 'Rechnungen' },
  ]},
  { group: 'Sonstiges', scope: 'flex', items: [
    { id: 'sonstiges', label: 'Sonstiges Dokument', extractable: false, category: 'Sonstiges' },
  ]},
];

// ════════════════════════════════════════════════════════════════════
// DOC FIELDS SPEC — SOLL-Felder pro Dokumenttyp
// Quelle: Gutachten_Datenstruktur_VorbereitungDokumente_Inhalt.csv (V&L-Spec)
// Wird verwendet im BulkUploadModal Field-Review:
//   - Pro Doc-Typ ALLE SOLL-Felder anzeigen, auch wenn KI sie nicht extrahiert hat
//   - Pre-fill mit extrahierten Werten, leere Felder bleiben manuell editierbar
//   - Beim Übernehmen: extrahierte + manuell ergänzte Werte zusammen speichern
// ════════════════════════════════════════════════════════════════════

const DOC_FIELDS_SPEC_FRONTEND = {
  altlasten: {
    label: 'Altlastenauskunft',
    fields: [
      { id: 'auskunftsart',  label: 'Auskunftsart', type: 'phrase',  default: 'Altlasten' },
      { id: 'datum',         label: 'Datum der Auskunft', type: 'date', required: true },
      { id: 'auskunftgeber', label: 'Auskunftgeber', type: 'phrase',
        placeholder: 'z.B. der Stadt Schwabach', required: true },
      { id: 'inhalt',        label: 'Inhalt der Auskunft', type: 'text',
        placeholder: 'z.B. "keine Eintragung" oder Volltext bei Eintrag', required: true,
        helpText: 'Bei "Eintrag vorhanden" muss der komplette Auskunftstext erfasst werden.' },
    ],
  },

  gebaeudeversicherung: {
    label: 'Gebäudeversicherung',
    fields: [
      { id: 'gesellschaft',       label: 'Versicherungsgesellschaft', type: 'phrase',
        placeholder: 'z.B. Allianz Versicherungs-AG', required: true },
      { id: 'versicherungssumme', label: 'Versicherungssumme', type: 'currency' },
      { id: 'baujahr_neuwert',    label: 'Baujahr (gleitend Neuwert)', type: 'number',
        placeholder: 'z.B. 1985',
        helpText: 'Bezugsjahr für den gleitenden Neuwert lt. Police' },
    ],
  },

  denkmalschutz: {
    label: 'Denkmalschutz',
    fields: [
      { id: 'eintrag_vorhanden', label: 'Eintrag in Denkmalliste', type: 'boolean', required: true },
      { id: 'art', label: 'Art des Eintrags', type: 'enum',
        options: [
          { value: 'baudenkmal', label: 'Baudenkmal' },
          { value: 'ensemble', label: 'Ensemble' },
          { value: 'bodendenkmal', label: 'Bodendenkmal' },
        ],
        condition: { kind: 'truthy', field: 'eintrag_vorhanden' } },
      { id: 'aktennummer', label: 'Aktennummer', type: 'phrase',
        placeholder: 'z.B. D-5-74-112-113',
        condition: { kind: 'truthy', field: 'eintrag_vorhanden' } },
      { id: 'bezeichnung', label: 'Bezeichnung / Objekt', type: 'phrase',
        placeholder: 'z.B. Melanchthon-Gymnasium',
        condition: { kind: 'truthy', field: 'eintrag_vorhanden' } },
      { id: 'beschreibung', label: 'Kurzbeschreibung', type: 'text',
        condition: { kind: 'truthy', field: 'eintrag_vorhanden' } },
    ],
  },

  feuerstaette: {
    label: 'Schornsteinfeger / Feuerstättenbescheid',
    fields: [
      { id: 'datum', label: 'Datum der Auskunft', type: 'date' },
      { id: 'auskunftgeber', label: 'Bezirksschornsteinfeger', type: 'phrase',
        placeholder: 'z.B. Bezirksschornsteinfeger Max Mustermann, 90765 Fürth' },
      { id: 'maengel', label: 'Mängel', type: 'text',
        placeholder: 'z.B. "keine Mängel" oder Liste der Mängel' },
      { id: 'messbescheinigung_datum', label: 'Datum Messbescheinigung', type: 'date' },
      { id: 'messbescheinigung_ergebnis', label: 'Ergebnis Messbescheinigung', type: 'text' },
      { id: 'heizungsart', label: 'Heizungsart', type: 'phrase',
        placeholder: 'z.B. Gaszentralheizung' },
      { id: 'fabrikat', label: 'Fabrikat', type: 'phrase', placeholder: 'z.B. Bosch / Viessmann' },
      { id: 'typ_heizung', label: 'Typ Heizung', type: 'phrase' },
      { id: 'baujahr_heizung', label: 'Baujahr Heizung', type: 'number' },
      { id: 'typ_brenner', label: 'Typ Brenner', type: 'phrase' },
      { id: 'baujahr_brenner', label: 'Baujahr Brenner', type: 'number' },
      { id: 'tank_material', label: 'Tank-Material', type: 'phrase',
        placeholder: 'z.B. Kunststoff / Stahl' },
      { id: 'tank_volumen', label: 'Tank-Fassungsvermögen (l)', type: 'phrase',
        placeholder: 'z.B. jeweils 1.000 l, gesamt 5.000 l' },
      { id: 'tank_anzahl', label: 'Anzahl Tanks', type: 'number' },
      { id: 'baujahr_laut_auskunft', label: 'Baujahr laut Auskunft', type: 'number',
        helpText: 'Falls vom Bezirksschornsteinfeger eigenständig angegeben' },
    ],
  },

  weg_auskunft: {
    label: 'WEG-Auskunft (Fragebogen)',
    fields: [
      { id: 'verwaltung_name', label: 'Verwaltung', type: 'phrase',
        placeholder: 'z.B. H2 Immobilienverwaltung GmbH' },
      { id: 'verwaltung_adresse', label: 'Anschrift Verwaltung', type: 'text',
        placeholder: 'z.B. Donaustraße 3, 91052 Erlangen' },
      { id: 'verwaltung_ansprechpartner', label: 'Ansprechpartner', type: 'phrase' },
      { id: 'verwaltung_telefon', label: 'Telefonnummer', type: 'phone' },
      { id: 'instandhaltungsruecklage', label: 'Instandhaltungsrücklage', type: 'currency' },
      { id: 'instandhaltungsruecklage_datum', label: 'Stichtag Instandhaltungsrücklage', type: 'date' },
      { id: 'wertrelevante_beschluesse', label: 'Wertrelevante Beschlüsse', type: 'text',
        placeholder: 'z.B. Aufzugerneuerung — beschlossen am 11.09.2025, Kosten ca. 80.000 €' },
      { id: 'sonderumlagen', label: 'Sonderumlagen', type: 'text',
        placeholder: 'z.B. "Nein" oder Beschreibung der Umlagen' },
      { id: 'ertraege_gemeinschaftseigentum', label: 'Erträge Gemeinschaftseigentum', type: 'text',
        placeholder: 'z.B. "Es bestehen keine Erträge."' },
      { id: 'hausgeldrueckstaende', label: 'Hausgeldrückstände insgesamt', type: 'currency' },
      { id: 'hausgeld_sondereigentum', label: 'Rückstände des Sondereigentums', type: 'text',
        placeholder: 'z.B. "Es bestehen keine Rückstände."' },
      { id: 'baujahr', label: 'Baujahr Gemeinschaftseigentum', type: 'number' },
      { id: 'modernisierungen_ge', label: 'Modernisierungen (Gemeinschaftseigentum)', type: 'text',
        placeholder: 'z.B. Dachsanierung 2018, Fassade 2021' },
      { id: 'lage_im_gebaeude', label: 'Lage im Gebäude', type: 'phrase',
        placeholder: 'z.B. 2. OG rechts' },
      { id: 'wohnflaeche', label: 'Wohnfläche Sondereigentum (m²)', type: 'number' },
      { id: 'vermietungsstatus', label: 'Vermietungsstatus', type: 'text',
        placeholder: 'z.B. Vermietet (650 € kalt) oder Eigennutzung' },
      { id: 'vergleichsmieten', label: 'Vergleichsmieten im Haus', type: 'text',
        placeholder: 'z.B. 9,50 €/m² Bestand, 12 €/m² Neuvermietung' },
      { id: 'laufzeit_verwaltervertrag', label: 'Laufzeit Verwaltervertrag', type: 'phrase',
        placeholder: 'z.B. bis 31.12.2027' },
      { id: 'hausgeld_hoehe', label: 'Aktuelles Hausgeld (gesamt)', type: 'currency' },
      { id: 'hausgeld_anteil_ihr', label: 'davon Anteil Instandhaltungsrücklage', type: 'currency' },
      { id: 'gebaeudeversicherung_vorhanden', label: 'Gebäudeversicherung vorhanden', type: 'boolean' },
      { id: 'gebaeudeversicherung_gesellschaft', label: 'Versicherungsgesellschaft', type: 'phrase',
        condition: { kind: 'truthy', field: 'gebaeudeversicherung_vorhanden' } },
      { id: 'energieausweis_vorhanden', label: 'Energieausweis vorhanden', type: 'boolean' },
      { id: 'energieausweis_datum', label: 'Datum Energieausweis', type: 'date',
        condition: { kind: 'truthy', field: 'energieausweis_vorhanden' } },
      { id: 'energieausweis_typ', label: 'Art des Ausweises', type: 'enum',
        options: [
          { value: 'verbrauchsausweis', label: 'Verbrauchsausweis' },
          { value: 'bedarfsausweis', label: 'Bedarfsausweis' },
        ],
        condition: { kind: 'truthy', field: 'energieausweis_vorhanden' } },
      { id: 'endenergie', label: 'Endenergieverbrauch (kWh/m²·a)', type: 'number',
        condition: { kind: 'truthy', field: 'energieausweis_vorhanden' } },
      { id: 'effizienzklasse', label: 'Energieeffizienzklasse', type: 'enum',
        options: [
          { value: 'A+', label: 'A+' }, { value: 'A', label: 'A' },
          { value: 'B', label: 'B' }, { value: 'C', label: 'C' },
          { value: 'D', label: 'D' }, { value: 'E', label: 'E' },
          { value: 'F', label: 'F' }, { value: 'G', label: 'G' },
          { value: 'H', label: 'H' },
        ],
        condition: { kind: 'truthy', field: 'energieausweis_vorhanden' } },
    ],
  },

  erschliessung: {
    label: 'Erschließung (Straße / Wasser / Kanal)',
    fields: [
      { id: 'datum', label: 'Datum der Auskunft', type: 'date' },
      { id: 'auskunftgeber', label: 'Auskunftgeber', type: 'phrase',
        placeholder: 'z.B. der Stadt Schwabach' },
      { id: 'beitraege_status', label: 'Status Erschließungsbeiträge', type: 'enum',
        options: [
          { value: 'abgegolten', label: 'abgegolten' },
          { value: 'noch_offen', label: 'noch offen' },
          { value: 'teilweise_offen', label: 'teilweise offen' },
          { value: 'unbekannt', label: 'unbekannt' },
        ] },
      { id: 'inhalt_strasse', label: 'Erschließungsbeitrag Straße', type: 'text',
        placeholder: 'z.B. "abgegolten" oder offener Betrag' },
      { id: 'inhalt_wasser', label: 'Erschließungsbeitrag Wasser', type: 'text' },
      { id: 'inhalt_kanal', label: 'Erschließungsbeitrag Kanal', type: 'text' },
      { id: 'grundstuecksentwaesserung', label: 'Grundstücksentwässerungsanlage', type: 'text',
        placeholder: 'z.B. "nicht bekannt" oder Details' },
    ],
  },

  baurechtsauskunft: {
    label: 'Baurechtliche Auskunft',
    fields: [
      { id: 'datum', label: 'Datum der Auskunft', type: 'date' },
      { id: 'auskunftgeber', label: 'Auskunftgeber', type: 'phrase',
        placeholder: 'z.B. der Stadt Ingolstadt' },
      { id: 'bplan_nr', label: 'Nr. / Titel B-Plan', type: 'phrase',
        placeholder: 'z.B. qualifizierter B-Plan Nr. 114, 4. Änderung' },
      { id: 'bplan_rechtskraft', label: 'B-Plan rechtskräftig seit', type: 'date' },
      { id: 'art_nutzung', label: 'Art der baulichen Nutzung', type: 'phrase',
        placeholder: 'z.B. Allgemeines Wohngebiet (WA)' },
      { id: 'grz', label: 'GRZ', type: 'number', placeholder: 'z.B. 0,4' },
      { id: 'gfz', label: 'GFZ', type: 'number', placeholder: 'z.B. 1,2' },
      { id: 'geschosse_max', label: 'Vollgeschosse (max.)', type: 'phrase',
        placeholder: 'z.B. I, IV, XI' },
      { id: 'bauweise', label: 'Bauweise', type: 'phrase',
        placeholder: 'z.B. geschlossene Bauweise (g)' },
      { id: 'dachform', label: 'Dachform', type: 'phrase',
        placeholder: 'z.B. Flachdach (FD)' },
      { id: 'baulinie', label: 'Baulinie', type: 'text',
        placeholder: 'z.B. Baulinie festgesetzt entlang der Hauptstraße' },
      { id: 'baugrenze', label: 'Baugrenze', type: 'text' },
      { id: 'bebauungstiefe', label: 'Bebauungstiefe', type: 'text' },
      { id: 'flaechennutzungsplan', label: 'Flächennutzungsplan', type: 'phrase',
        placeholder: 'z.B. Flächennutzungsplan der Stadt Ingolstadt' },
      { id: 'fnp_rechtswirksam', label: 'FNP rechtswirksam', type: 'phrase',
        placeholder: 'z.B. April 1996' },
      { id: 'fnp_art_nutzung', label: 'FNP Art der Nutzung', type: 'phrase',
        placeholder: 'z.B. Wohnbaufläche' },
      { id: 'satzungen', label: 'Satzungen', type: 'text',
        placeholder: 'z.B. Abstandsflächen-, Garagen- und Stellplatzsatzung' },
      { id: 'bplan_beschlossen', label: 'B-Plan beschlossen / im Verfahren', type: 'text',
        placeholder: 'z.B. Einleitungsbeschluss vom …' },
      { id: 'veraenderungssperre', label: 'Veränderungssperre vorhanden', type: 'text',
        placeholder: 'z.B. Eine Veränderungssperre nach §14 BauGB liegt vor' },
      { id: 'gemeinderecht', label: 'Gemeinderecht / Eintragungen', type: 'text',
        placeholder: 'z.B. Geh- und Fahrtrecht zugunsten …' },
    ],
  },

  grundbuchauszug: {
    label: 'Grundbuchauszug',
    fields: [
      { id: 'grundbuch_art', label: 'Art des Grundbuchs', type: 'enum',
        options: [
          { value: 'wohnungsgrundbuch', label: 'Wohnungsgrundbuch' },
          { value: 'teileigentumsgrundbuch', label: 'Teileigentumsgrundbuch' },
          { value: 'wohnungserbbaurecht', label: 'Wohnungserbbaurecht' },
          { value: 'normales_grundbuch', label: 'Grundbuch (normal)' },
        ] },
      { id: 'amtsgericht', label: 'Amtsgericht', type: 'phrase' },
      { id: 'gemarkung', label: 'Gemarkung', type: 'phrase' },
      { id: 'blatt', label: 'Blatt-Nr.', type: 'number' },
      { id: 'abdruck_vom', label: 'Abdruck-Datum', type: 'date' },
      { id: 'auffaelligkeiten', label: 'Auffälligkeiten', type: 'text',
        placeholder: 'z.B. Abtretungserklärung FlNr. 213; Rangverhältnisse …' },
    ],
  },

  demografie: {
    label: 'Demografie / Bevölkerungsentwicklung',
    fields: [
      { id: 'quelle_name', label: 'Quellenbezeichnung', type: 'phrase',
        placeholder: 'z.B. Demographisches Profil für die Kreisfreie Stadt Fürth' },
      { id: 'zeitraum', label: 'Zeitraum', type: 'phrase',
        placeholder: 'z.B. 2023 bis 2043' },
      { id: 'jahr_basis', label: 'Basisjahr', type: 'number' },
      { id: 'jahr_mitte', label: 'Mitteljahr', type: 'number' },
      { id: 'jahr_ende', label: 'Endjahr', type: 'number' },
      { id: 'bev_basis', label: 'Bevölkerung Basisjahr', type: 'number' },
      { id: 'bev_mitte', label: 'Bevölkerung Mitteljahr', type: 'number' },
      { id: 'bev_ende', label: 'Bevölkerung Endjahr', type: 'number' },
      { id: 'veraendg_insgesamt', label: 'Veränderung gesamt (%)', type: 'number' },
      { id: 'veraendg_u18', label: 'Veränderung unter 18 (%)', type: 'number' },
      { id: 'veraendg_18_40', label: 'Veränderung 18–40 (%)', type: 'number' },
      { id: 'veraendg_40_65', label: 'Veränderung 40–65 (%)', type: 'number' },
      { id: 'veraendg_65plus', label: 'Veränderung 65+ (%)', type: 'number' },
    ],
  },

  energieausweis: {
    label: 'Energieausweis (Einzelobjekt, keine WEG)',
    fields: [
      { id: 'art_ausweis', label: 'Art des Ausweises', type: 'enum',
        options: [
          { value: 'verbrauchsausweis', label: 'Verbrauchsausweis' },
          { value: 'bedarfsausweis', label: 'Bedarfsausweis' },
        ] },
      { id: 'datum', label: 'Ausstellungsdatum', type: 'date' },
      { id: 'gueltig_bis', label: 'Gültig bis', type: 'date' },
      { id: 'endenergie', label: 'Endenergiebedarf/-verbrauch (kWh/m²·a)', type: 'number' },
      { id: 'primaerenergie', label: 'Primärenergie (kWh/m²·a)', type: 'number' },
      { id: 'effizienzklasse', label: 'Energieeffizienzklasse', type: 'enum',
        options: [
          { value: 'A+', label: 'A+' }, { value: 'A', label: 'A' },
          { value: 'B', label: 'B' }, { value: 'C', label: 'C' },
          { value: 'D', label: 'D' }, { value: 'E', label: 'E' },
          { value: 'F', label: 'F' }, { value: 'G', label: 'G' },
          { value: 'H', label: 'H' },
        ] },
      { id: 'energietraeger', label: 'Energieträger', type: 'phrase',
        placeholder: 'z.B. Fernwärme / Erdgas / Heizöl' },
      { id: 'baujahr_heizung', label: 'Baujahr Heizung', type: 'number' },
      { id: 'baujahr_gebaeude', label: 'Baujahr Gebäude (lt. Ausweis)', type: 'number' },
      { id: 'wohnflaeche', label: 'Wohnfläche lt. Ausweis (m²)', type: 'number' },
      { id: 'registriernummer', label: 'Registriernummer', type: 'phrase',
        placeholder: 'z.B. BY-2018-002129051' },
    ],
  },
};

// Mapping zwischen UPLOAD_TYPES Doc-IDs und DOC_FIELDS_SPEC-Keys.
// (Mehrere UPLOAD_TYPES können zur selben Spec gehören, z.B. WEG-Protokoll / WEG-Hausgeld
// teilen sich die "weg_auskunft"-Spec.)
// IDs müssen exakt mit UPLOAD_TYPES.items.id übereinstimmen.
const DOC_REGISTRY_TO_SPEC = {
  altlasten: 'altlasten',
  denkmalschutz: 'denkmalschutz',
  feuerstaette: 'feuerstaette',
  messbescheinigung: 'feuerstaette',
  weg_verwalter: 'weg_auskunft',
  weg_protokoll: 'weg_auskunft',
  weg_hausgeld: 'weg_auskunft',
  weg_wirtschaftsplan: 'weg_auskunft',
  erschliessung_strasse: 'erschliessung',
  erschliessung_wasser: 'erschliessung',
  erschliessung_kanal: 'erschliessung',
  baurechtsauskunft: 'baurechtsauskunft',
  bebauungsplan: 'baurechtsauskunft',
  flaechennutzungsplan: 'baurechtsauskunft',
  grundbuchauszug: 'grundbuchauszug',
  demografie: 'demografie',
  energieausweis: 'energieausweis',
};

// Helper: gib die SOLL-Felder-Spec für eine Doc-Type-ID zurück (oder null)
const getDocSpecForType = (typId) => {
  if (!typId) return null;
  const specKey = DOC_REGISTRY_TO_SPEC[typId];
  return specKey ? DOC_FIELDS_SPEC_FRONTEND[specKey] : null;
};

// Helper für conditional fields (Spec-condition wertet gegen aktuelle Werte aus)
const docFieldVisible = (cond, values) => {
  if (!cond) return true;
  switch (cond.kind) {
    case 'eq':       return values[cond.field] === cond.value;
    case 'neq':      return values[cond.field] !== cond.value;
    case 'truthy':   return !!values[cond.field];
    case 'falsy':    return !values[cond.field];
    case 'one_of':   return cond.values?.includes(values[cond.field]);
    default:         return true;
  }
};

const SCOPE_HELP_TEXT = {
  auftrag: <>Wird dem <strong>Auftrag</strong> zugeordnet. Gerichtsbeschluss und Anschreiben füllen Aktenzeichen, Beteiligte und Fristen automatisch aus.</>,
  gutachten: <>Wird dem <strong>aktiven Gutachten</strong> zugeordnet. Gilt für alle Objekte innerhalb des Gutachtens.</>,
  objekt: <>Wird einem <strong>einzelnen Bewertungsobjekt</strong> zugeordnet.</>,
  flex: <>Wird als sonstiges Dokument abgelegt. Die KI versucht, Basis-Felder (Datum, Absender) zu extrahieren.</>,
};

// Feld-Beschriftungen für den Review-Dialog (deutsche Labels)
const FELD_LABELS = {
  aktenzeichen: 'Aktenzeichen',
  gerichtstyp: 'Gerichtstyp',
  auftragsart: 'Auftragsart',
  auftragstyp: 'Auftragstyp',
  auftraggeber: 'Auftraggeber',
  auftragsbeschreibung: 'Auftragsbeschreibung',
  verfahrensart: 'Verfahrensart',
  sache: 'Sache',
  wegen: 'Wegen',
  kostenvorschuss: 'Kostenvorschuss',
  vereinbarter_stundensatz: 'Vereinbarter Stundensatz',
  ausfertigungen: 'Ausfertigungen',
  abgabe_extern: 'Abgabefrist (extern)',
  auftragseingang: 'Auftragseingang',
  beschlussdatum: 'Beschlussdatum',
  zweck: 'Zweck',
  objektadresse: 'Objektadresse',
  flur: 'Flur',
  flurstueck: 'Flurstück',
  gemarkung: 'Gemarkung',
  groesse_qm: 'Grundstücksgröße (m²)',
  bruchteilseigentum: 'Bruchteilseigentum',
  mea: 'Miteigentumsanteile',
  sondereigentumseinheit: 'Sondereigentumseinheit',
  abt_2_eintragungen: 'Eintragungen Abt. II',
  abt_3_eintragungen: 'Eintragungen Abt. III',
  belastungen_abt_2: 'Belastungen Abt. II',
  grundbuchblatt: 'Grundbuchblatt',
  wirtschaftsart: 'Wirtschaftsart',
  // Bebauungsplan
  art_bauliche_nutzung: 'Art der baulichen Nutzung',
  grz: 'GRZ',
  gfz: 'GFZ',
  vollgeschosse_zulaessig: 'Zulässige Vollgeschosse',
  bauweise: 'Bauweise',
  dachform: 'Dachform',
  bplan_nr: 'B-Plan Nr.',
  bplan_status: 'B-Plan Status',
  festsetzungen_text: 'Festsetzungen',
  // Bodenrichtwert
  bodenrichtwert: 'Bodenrichtwert (EUR/m²)',
  brw_stichtag: 'BRW Stichtag',
  brw_zone: 'Richtwertzone',
  brw_entwicklungszustand: 'Entwicklungszustand',
  brw_nutzungsart: 'BRW Nutzungsart',
  brw_gfz: 'BRW Bezugs-GFZ',
  // Energieausweis (DB-Feldnamen)
  endenergie: 'Endenergie (kWh/m²a)',
  primaerenergie: 'Primärenergie (kWh/m²a)',
  effizienzklasse: 'Effizienzklasse',
  energieausweis_typ: 'Ausweis-Typ',
  energietraeger: 'Energieträger',
  baujahr_heizung: 'Baujahr Heizung',
  energieausweis_gueltig_bis: 'Energieausweis gültig bis',
  energieausweis_registriernummer: 'EA Registriernummer',
  // Energieausweis (Extraktions-Feldnamen)
  ausweis_typ: 'Ausweis-Typ',
  wesentlicher_energietraeger: 'Energieträger',
  gueltig_bis: 'Gültig bis',
  registriernummer: 'Registriernummer',
  baujahr_gebaeude: 'Baujahr Gebäude',
  wohnflaeche_ea: 'Wohnfläche (m²)',
  erneuerbare_energien: 'Erneuerbare Energien',
  // Grundbuch (komplexe Felder)
  bestandsverzeichnis: 'Bestandsverzeichnis',
  abteilung_i: 'Abteilung I (Eigentümer)',
  abteilung_ii: 'Abteilung II (Lasten/Beschränkungen)',
  abteilung_iii: 'Abteilung III (Hypotheken/Grundschulden)',
  herrschvermerk: 'Herrschvermerk',
  // Altlasten
  ergebnis: 'Ergebnis',
  kataster_system: 'Kataster-System',
  behoerde: 'Behörde',
  auskunft_datum: 'Datum der Auskunft',
  eintrag_text: 'Eintrag (Volltext)',
  // Baurecht
  auskunftgeber: 'Auskunftgeber',
  bplan_vorhanden: 'B-Plan vorhanden',
  bplan_typ: 'B-Plan Typ',
  bplan_name: 'B-Plan Name',
  bplan_rechtskraeftig_seit: 'B-Plan rechtskräftig seit',
  vollgeschosse_zulaessig: 'Zul. Vollgeschosse',
  dachneigung: 'Dachneigung',
  paragraph_34_35: '§ 34/35 BauGB',
  baulinie: 'Baulinie',
  baugrenze: 'Baugrenze',
  bebauungstiefe: 'Bebauungstiefe',
  bplan_aufstellung_beschlossen: 'B-Plan Aufstellung',
  fnp_vorhanden: 'FNP vorhanden',
  fnp_bezeichnung: 'FNP Bezeichnung',
  fnp_rechtswirksam_seit: 'FNP rechtswirksam seit',
  fnp_darstellung: 'FNP Darstellung',
  veraenderungssperre: 'Veränderungssperre',
  satzungen_vorhanden: 'Satzungen vorhanden',
  satzungen_text: 'Satzungen',
  erschliessung_abgerechnet: 'Erschließung abgerechnet',
  erschliessung_bemerkung: 'Erschließung Bemerkung',
  entwasserung_untersucht: 'Entwässerung untersucht',
  entwasserung_bemerkung: 'Entwässerung Bemerkung',
  gemeinderecht: 'Gemeindliches Vorkaufsrecht',
  // Erschließung
  strassenerschliessung: 'Straßenerschließung',
  strassenerschliessung_betrag: 'Erschließung Betrag (EUR)',
  strassenerschliessung_text: 'Erschließung Wortlaut',
  strassenausbaubeitraege: 'Straßenausbaubeiträge',
  strassenausbaubeitraege_betrag: 'Ausbaubeiträge Betrag (EUR)',
  herstellungsbeitraege_wasser: 'Herstellungsbeiträge Wasser',
  herstellungsbeitraege_wasser_betrag: 'Wasser Betrag (EUR)',
  herstellungsbeitraege_wasser_text: 'Wasser Wortlaut',
  herstellungsbeitraege_kanal: 'Herstellungsbeiträge Kanal',
  herstellungsbeitraege_kanal_betrag: 'Kanal Betrag (EUR)',
  herstellungsbeitraege_kanal_text: 'Kanal Wortlaut',
  grundstuecksentwaesserung: 'Grundstücksentwässerung',
  auskunft_person: 'Auskunft-Person',
  auskunft_art: 'Auskunftsart',
  // Gebäudeversicherung
  versicherer: 'Versicherer',
  versicherungsscheinnummer: 'Versicherungsscheinnr.',
  versicherungssumme_1914: 'Vers.-Summe 1914 (Mark)',
  versicherungssumme_gleitend: 'Vers.-Summe gleitend (EUR)',
  versicherungsumfang: 'Versicherungsumfang',
  selbstbeteiligung: 'Selbstbeteiligung',
  versicherungsbeginn: 'Versicherungsbeginn',
  praemie: 'Jahresprämie (EUR)',
  // Denkmalschutz
  denkmalschutz_status: 'Denkmalschutz-Status',
  // Schornsteinfeger
  auskunftsart: 'Auskunftsart',
  maengel: 'Mängel',
  messbescheinigung_datum: 'Messbescheinigung Datum',
  messbescheinigung_ergebnis: 'Messbescheinigung Ergebnis',
  heizungsart: 'Heizungsart',
  fabrikat: 'Fabrikat',
  typ_heizung: 'Typ Heizung',
  typ_brenner: 'Typ Brenner',
  baujahr_brenner: 'Baujahr Brenner',
  tank_material: 'Tank Material',
  tank_fassungsvermoegen: 'Tank Fassungsvermögen',
  tank_anzahl: 'Anzahl Tanks',
  baujahr_objekt: 'Baujahr (lt. Auskunft)',
  // WEG
  verwaltung: 'Hausverwaltung',
  ansprechpartner: 'Ansprechpartner',
  telefonnummer: 'Telefonnummer',
  laufzeit_verwaltervertrag: 'Laufzeit Verwaltervertrag',
  hausgeld: 'Hausgeld (EUR/Monat)',
  hausgeld_anteil_ruecklage: 'Hausgeld Rücklage-Anteil (EUR)',
  instandhaltungsruecklage: 'Instandhaltungsrücklage (EUR)',
  instandhaltungsruecklage_datum: 'Rücklage Stichtag',
  hausgeldsrueckstaende: 'Hausgeldrückstände (EUR)',
  hausgeld_sondereigentum: 'Hausgeld Sondereigentum',
  sonderumlagen: 'Sonderumlagen',
  ertraege_gemeinschaftseigentum: 'Erträge Gemeinschaftseigentum',
  wertrelevante_beschluesse: 'Wertrelevante Beschlüsse',
  wertrelevante_beschluesse_datum: 'Beschluss-Datum',
  wertrelevante_beschluesse_betrag: 'Beschluss Finanzierung',
  modernisierungen: 'Modernisierungen (GE)',
  lage_im_gebaeude: 'Lage im Gebäude',
  vermietungsstatus: 'Vermietungsstatus',
  vergleichsmieten: 'Vergleichsmieten',
  versicherungsstatus: 'Versicherungsstatus',
  energieausweis_vorhanden: 'Energieausweis vorhanden',
  energieausweis_datum: 'Energieausweis Datum',
  endenergieverbrauch: 'Endenergieverbrauch (kWh/m²a)',
  energieeffizienzklasse: 'Energieeffizienzklasse',
  // Grundbuch (zusätzlich)
  grundbuch_art: 'Grundbuch-Art',
  kaufvertrag_datum: 'Kaufvertrag-Datum',
  auffaelligkeiten: 'Auffälligkeiten',
  // Energieausweis (zusätzlich)
  ausstellungsdatum: 'Ausstellungsdatum',
  // Multi-Stichtag/Verkehrswert
  wertermittlungsstichtag: 'Wertermittlungsstichtag',
  wertermittlungsstichtag_2: 'Wertermittlungsstichtag 2',
  wertermittlungsstichtag_3: 'Wertermittlungsstichtag 3',
  qualitaetsstichtag: 'Qualitätsstichtag',
  qualitaetsstichtag_2: 'Qualitätsstichtag 2',
  qualitaetsstichtag_3: 'Qualitätsstichtag 3',
  verkehrswert: 'Verkehrswert',
  verkehrswert_2: 'Verkehrswert 2',
  verkehrswert_3: 'Verkehrswert 3',
  wertermittlungsmethode: 'Wertermittlungsmethode',
  zuschlag_datum: 'Zuschlag vom',
  zuschlag_betrag: 'Zuschlag zu (EUR)',
  versteigerungsobjekte: 'Versteigerungsobjekte (WEG)',
  objekttyp: 'Objekttyp',
  // ── WEG-Auskunft ──
  verwaltung: 'Hausverwaltung',
  verwaltervertrag_bis: 'Verwaltervertrag bis',
  ansprechpartner: 'Ansprechpartner',
  ansprechpartner_tel: 'Ansprechpartner Telefon',
  ruecklage: 'Instandhaltungsrücklage (EUR)',
  ruecklage_datum: 'Rücklage Stand',
  beschluesse: 'Wertrelevante Beschlüsse',
  beschluss_datum: 'Beschluss Datum',
  beschluss_betrag: 'Beschluss Betrag',
  hausgeld: 'Hausgeld',
  hausgeld_ruecklage_anteil: 'Hausgeld Rücklagen-Anteil',
  rueckstaende: 'Hausgeldrückstände',
  sonderumlagen: 'Sonderumlagen',
  sonderumlagen_text: 'Sonderumlagen (Details)',
  ertraege_gemein: 'Erträge Gemeinschaftseigentum',
  modernisierungen: 'Modernisierungen',
  lage_im_gebaeude: 'Lage im Gebäude',
  wohnflaeche: 'Wohnfläche (m²)',
  vermietungsstatus: 'Vermietungsstatus',
  miete_kalt: 'Kaltmiete',
  vergleichsmieten: 'Vergleichsmieten',
  gebaeudeversicherung_status: 'Gebäudeversicherung Status',
  versicherungsstatus_detail: 'Versicherungsstatus (Details)',
  energieausweis_vorh: 'Energieausweis vorhanden',
  energieausweis_datum: 'Energieausweis Datum',
  energieklasse: 'Energieeffizienzklasse',
  // ── Gebäudeversicherung ──
  gesellschaft: 'Versicherungsgesellschaft',
  versicherungssumme: 'Versicherungssumme',
  baujahr_neuwert: 'Baujahr (Neuwert-Basis)',
  umfang_versicherungsschutz: 'Umfang Versicherungsschutz',
  selbstbehalt_leitungswasser: 'Selbstbehalt Leitungswasser',
  selbstbehalt_elementar: 'Selbstbehalt Elementar',
  modernisierungskosten: 'Modernisierungskosten',
  versicherungsschein_nr: 'Versicherungsschein-Nr.',
  // ── Feuerstätte / Schornsteinfeger ──
  bescheid_datum: 'Bescheid Datum',
  schornsteinfeger: 'Schornsteinfeger',
  feuerstaetten: 'Feuerstätten',
  naechste_pruefung: 'Nächste Prüfung',
  maengel: 'Mängel',
  energietraeger: 'Energieträger',
  // ── Denkmalschutz ──
  eintrag_vorhanden: 'Eintrag vorhanden',
  art: 'Art Denkmalschutz',
  bezeichnung: 'Bezeichnung',
  datum: 'Datum',
  quelle: 'Quelle',
  // ── Diverse fehlende (Routing- und Auskunfts-Typen) ──
  sachverstaendiger: 'Sachverständiger',
  objekttyp_vermutung: 'Objekttyp (vermutet)',
  vollgeschosse: 'Vollgeschosse',
  brw_grundstuecksgroesse: 'BRW Grundstücksgröße (m²)',
  brw_geschosszahl: 'BRW Geschosszahl',
  sanierungsgebiet: 'Sanierungsgebiet',
  flurstuck: 'Flurstück',
  bemerkung: 'Bemerkung',
  baujahr: 'Baujahr',
  // ── Demografie ──
  gemeinde: 'Gemeinde',
  gemeindetyp: 'Gemeindetyp',
  landkreis: 'Landkreis',
  basisjahr: 'Basisjahr',
  prognosejahr_kurz: 'Prognosejahr (kurzfristig)',
  prognosejahr_lang: 'Prognosejahr (langfristig)',
  bevoelkerung_basisjahr: 'Bevölkerung Basisjahr',
  bevoelkerung_prognose_kurz: 'Bevölkerung Prognose (kurz)',
  bevoelkerung_prognose_lang: 'Bevölkerung Prognose (lang)',
  veraenderung_gesamt_pct: 'Veränderung gesamt (%)',
  veraenderung_unter_18_pct: 'Veränderung unter 18 (%)',
  veraenderung_18_40_pct: 'Veränderung 18–40 (%)',
  veraenderung_40_65_pct: 'Veränderung 40–65 (%)',
  veraenderung_65_plus_pct: 'Veränderung 65+ (%)',
  veraenderung_unter_3_pct: 'Veränderung unter 3 (%)',
  veraenderung_3_6_pct: 'Veränderung 3–6 (%)',
  veraenderung_6_10_pct: 'Veränderung 6–10 (%)',
  veraenderung_10_16_pct: 'Veränderung 10–16 (%)',
  veraenderung_16_19_pct: 'Veränderung 16–19 (%)',
  veraenderung_60_75_pct: 'Veränderung 60–75 (%)',
  veraenderung_75_plus_pct: 'Veränderung 75+ (%)',
  durchschnittsalter_basisjahr: 'Durchschnittsalter Basisjahr',
  durchschnittsalter_prognose: 'Durchschnittsalter Prognose',
  jugendquotient_basisjahr: 'Jugendquotient Basisjahr',
  jugendquotient_prognose: 'Jugendquotient Prognose',
  altenquotient_basisjahr: 'Altenquotient Basisjahr',
  altenquotient_prognose: 'Altenquotient Prognose',
  // ── Feuerstätte (Ergänzungen) ──
  adresse: 'Adresse',
  anzahl_tanks: 'Anzahl Tanks',
  baujahr_laut_auskunft: 'Baujahr (laut Auskunft)',
  aktennummer: 'Aktennummer',
  // ── Grundbuch (Ergänzungen) ──
  amtsgericht: 'Amtsgericht',
  abdruck_vom: 'Abdruck vom',
};

// DB-Tabellennamen → deutsche Anzeige-Labels für die Konflikt-UI.
// Maschinen-Bezeichnungen wie "auftraege" sollen dem Nutzer nie
// direkt unter die Augen kommen.
const TARGET_LABELS = {
  auftraege: 'Auftrag',
  gutachten: 'Gutachten',
  bewertungsobjekte: 'Bewertungsobjekt',
  beteiligte: 'Beteiligte:r',
};

function formatFieldValue(key, value) {
  if (value == null) return <span style={{ color: 'var(--text-tertiary)' }}>—</span>;
  if (typeof value === 'number') {
    if (key === 'kostenvorschuss') return `€${value.toLocaleString('de-DE')}`;
    if (key === 'groesse_qm') return `${value} m²`;
    return String(value);
  }
  if (key.startsWith('abgabe_') || key === 'auftragseingang' || key === 'beschlussdatum') {
    const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
    if (m) return `${m[3]}.${m[2]}.${m[1]}`;
  }
  // Enum-Werte großschreiben (amtsgericht → Amtsgericht)
  if (typeof value === 'string' && value.length > 1 && value === value.toLowerCase()) {
    return value.charAt(0).toUpperCase() + value.slice(1);
  }
  return String(value);
}

// Liefert die Liste der Haupt-Felder (ohne Listen wie beteiligte, eigentuemer)
function getScalarFields(fields, typ) {
  const skip = new Set(['beteiligte', 'eigentuemer', 'notizen', 'kontakt_anschrift', 'titel_vorschlag', 'versteigerungsobjekte']);
  // Für Grundbuch: objekttyp_vermutung → objekttyp umbenennen für UX
  return Object.keys(fields).filter(k => !skip.has(k) && fields[k] != null);
}

// Style-Helper für die Wahl-Buttons im Konflikt-Review
function conflictChoiceBtn(active, activeColor) {
  return {
    padding: '5px 12px', fontSize: 12, fontWeight: 600,
    border: `1px solid ${active ? activeColor : 'var(--border-light)'}`,
    background: active ? `${activeColor}15` : 'transparent',
    color: active ? activeColor : 'var(--text-secondary)',
    borderRadius: 'var(--radius-sm)',
    cursor: 'pointer',
    transition: 'all 0.12s',
  };
}

// ══════════════════════════════════════════════════════════════════
// UPLOAD-FLOW
// Vier Phasen: form → progress → review → applied  (+ conflicts bei Re-Upload)
// ══════════════════════════════════════════════════════════════════

const UploadModal = ({
  onClose, onApplied, objektContext,
  projektId, aktiverGutachtenId, session,
  workerUrl,
  prefillFile, prefillTyp, noAutoStart,
  queueContext,  // { position: 1-based, total, typLabel, nextTypLabel? }
  projektName, onRenameProjekt,  // für Quick-Create: editierbarer Projektname
  autoApply,  // true → Felder automatisch übernehmen ohne Review-Modal
}) => {
  const showToast = useToast();
  // Phase-Management
  const [phase, setPhase] = useState('form');  // 'form' | 'progress' | 'review' | 'applied'

  // Form-State
  const [selectedTypeId, setSelectedTypeId] = useState(prefillTyp || 'gerichtsbeschluss');
  const [selectedObjektIdx, setSelectedObjektIdx] = useState(0);
  const [file, setFile] = useState(prefillFile || null);
  const [formError, setFormError] = useState(null);

  // Progress-State
  const [progressStep, setProgressStep] = useState('uploading');
  const [progressPct, setProgressPct] = useState(0);
  const [progressError, setProgressError] = useState(null);
  const [isMock, setIsMock] = useState(false);

  // Review-State
  const [dokumentId, setDokumentId] = useState(null);
  const [extractedFields, setExtractedFields] = useState(null);
  const [acceptedFields, setAcceptedFields] = useState(new Set());
  const [acceptedBeteiligteIdx, setAcceptedBeteiligteIdx] = useState(new Set());
  const [customValues, setCustomValues] = useState({});
  const [editedBeteiligte, setEditedBeteiligte] = useState([]);   // Shadow-Copy für Inline-Edit
  const [editedEigentuemer, setEditedEigentuemer] = useState([]);
  const [editedTitle, setEditedTitle] = useState('');   // Dokumenten-Titel, editierbar vor Apply

  // Teilungserklärung-Sonderflow: Einheitenliste mit akzeptiert-Flag pro Einheit,
  // Fortschrittsanzeige während Multi-Objekt-Insert.
  const [teEinheiten, setTeEinheiten] = useState([]);
  const [teInsertProgress, setTeInsertProgress] = useState(null);  // { done, total, errors }
  const [applyError, setApplyError] = useState(null);
  const [applying, setApplying] = useState(false);
  const [pdfViewerUrl, setPdfViewerUrl] = useState(null);

  // Quick-Create: Editierbarer Projektname (nur bei neuem Projekt mit temp-Name)
  const isQuickCreate = projektName && projektName.startsWith('Neuer Auftrag vom');
  const [editedProjektName, setEditedProjektName] = useState(projektName || '');

  // WEG Auto-Apply: Bewertungsobjekte aus versteigerungsobjekte-Array erstellen
  const [wegCreating, setWegCreating] = useState(false);
  const [wegCreated, setWegCreated] = useState(false);
  const [wegError, setWegError] = useState(null);

  // Post-Apply-State: Konflikte aus dem Worker bei Re-Upload
  const [conflicts, setConflicts] = useState([]);
  const [forceOverwrite, setForceOverwrite] = useState([]);  // pro-Feld opt-in

  // Applied-State
  const [appliedSummary, setAppliedSummary] = useState(null);

  const fileInputRef = useRef(null);

  const objekte = objektContext || [];
  const registryTypes = useUploadTypes();
  const uploadTypes = registryTypes || UPLOAD_TYPES;
  const selectedType = uploadTypes
    .flatMap(g => g.items.map(item => ({ ...item, scope: g.scope })))
    .find(t => t.id === selectedTypeId);
  const scope = selectedType?.scope || 'auftrag';
  const extractable = selectedType?.extractable === true;
  // Picker zeigen wenn objekt-scope UND mehr als ein Objekt zur Auswahl steht
  const showObjektPicker = scope === 'objekt' && objekte.length > 1;
  // Ausgewähltes Objekt: bei Single-Objekt automatisch das erste, sonst vom Nutzer gewählt
  const selectedObjektId = scope === 'objekt' && objekte.length > 0
    ? objekte[Math.min(selectedObjektIdx, objekte.length - 1)]?.id
    : null;

  const handleFileSelect = (e) => {
    const f = e.target.files?.[0];
    if (f) {
      setFile(f);
      setFormError(null);
    }
  };

  const handleUpload = async () => {
    if (!file) {
      setFormError('Bitte eine Datei auswählen');
      return;
    }
    // Scope-Validierung: für objekt-scope brauchen wir ein konkretes Objekt
    if (scope === 'objekt' && !selectedObjektId) {
      setFormError('Für diesen Dokumenttyp muss ein Bewertungsobjekt existieren. Lege zuerst ein Gutachten mit Objekt an, dann den Upload wiederholen.');
      return;
    }
    if ((scope === 'gutachten' || scope === 'objekt') && !aktiverGutachtenId) {
      setFormError('Für diesen Dokumenttyp muss ein Gutachten existieren. Lege zuerst ein Gutachten an, dann den Upload wiederholen.');
      return;
    }
    setFormError(null);
    setPhase('progress');
    setProgressStep('uploading');
    setProgressPct(0);
    setProgressError(null);

    try {
      const formData = new FormData();
      formData.append('file', file);
      formData.append('project_id', projektId);
      formData.append('scope', scope);
      formData.append('typ', selectedTypeId);
      if (scope === 'gutachten' || scope === 'objekt') {
        formData.append('gutachten_id', aktiverGutachtenId);
      }
      if (scope === 'objekt' && selectedObjektId) {
        formData.append('objekt_id', selectedObjektId);
      }

      const res = await fetch(`${workerUrl}/api/extract/document`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${session.access_token}` },
        body: formData,
      });

      if (!res.ok) {
        const errBody = await res.json().catch(() => ({}));
        throw new Error(errBody.error || `HTTP ${res.status}`);
      }

      // SSE-Stream lesen
      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';
      let done = false;

      while (!done) {
        const { value, done: d } = await reader.read();
        done = d;
        if (value) buffer += decoder.decode(value, { stream: true });

        // Events aus Buffer parsen (doppelte Newline terminiert Event)
        let eventEnd;
        while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
          const block = buffer.slice(0, eventEnd);
          buffer = buffer.slice(eventEnd + 2);

          const eventMatch = block.match(/^event:\s*(\S+)/m);
          const dataMatch = block.match(/^data:\s*(.*)$/m);
          if (!eventMatch || !dataMatch) continue;

          const eventName = eventMatch[1];
          let data;
          try { data = JSON.parse(dataMatch[1]); } catch { continue; }

          if (eventName === 'progress') {
            setProgressStep(data.step || 'extracting');
            setProgressPct(data.pct || 0);
          } else if (eventName === 'result') {
            setDokumentId(data.dokument_id);
            const env = data.extracted_fields;

            // Non-extractable: kein Review nötig, direkt abschließen
            if (!env || !env.fields) {
              setPhase('done');
              if (onApplied) onApplied();
              return;
            }

            setExtractedFields(env);
            setIsMock(env?.is_mock === true);

            // PDF-URL für Inline-Viewer laden
            if (data.dokument_id) {
              ladeDokumentUrl(data.dokument_id, session, workerUrl).then(url => {
                if (url) setPdfViewerUrl(url);
              });
            }

            // Default-Auswahl: Alle Felder vorausgewählt.
            // Der Nutzer kann einzelne Felder abwählen falls sie bestehende Daten überschreiben würden.
            const scalars = getScalarFields(env.fields, selectedTypeId);
            setAcceptedFields(new Set(scalars));

            // Beteiligte: Shadow-Copy für Inline-Edit
            if (Array.isArray(env.fields.beteiligte)) {
              setAcceptedBeteiligteIdx(new Set(env.fields.beteiligte.map((_, i) => i)));
              setEditedBeteiligte(env.fields.beteiligte.map(b => ({ ...b })));
            } else {
              setEditedBeteiligte([]);
            }
            // Eigentümer: analog
            if (Array.isArray(env.fields.eigentuemer)) {
              setEditedEigentuemer(env.fields.eigentuemer.map(e => ({ ...e })));
            } else {
              setEditedEigentuemer([]);
            }

            // Dokument-Titel: Vorschlag aus KI, Fallback auf Dateiname ohne Extension.
            // Der User kann den Titel im Review-Dialog anpassen, bevor er übernommen wird.
            const suggested = (typeof env.fields?.titel_vorschlag === 'string' && env.fields.titel_vorschlag.trim())
              ? env.fields.titel_vorschlag.trim()
              : (file?.name || '').replace(/\.[^.]+$/, '');
            setEditedTitle(suggested);

            // Quick-Create: Projektname-Vorschlag aus extrahierten Daten
            if (isQuickCreate && env.fields) {
              const parts = [];
              // Aktenzeichen
              if (env.fields.aktenzeichen) parts.push(env.fields.aktenzeichen);
              // Parteinamen (erste 2 Beteiligte)
              const bet = Array.isArray(env.fields.beteiligte) ? env.fields.beteiligte : [];
              const namen = bet.slice(0, 2).map(b => {
                const n = b.name || '';
                // Nachname extrahieren (letztes Wort oder nach Komma)
                if (n.includes(',')) return n.split(',')[0].trim();
                const words = n.trim().split(/\s+/);
                return words[words.length - 1];
              }).filter(Boolean);
              if (namen.length > 0) parts.push(namen.join(' ./. '));
              // Adresse als Fallback
              if (parts.length === 0 && env.fields.adresse) parts.push(env.fields.adresse);
              if (parts.length > 0) setEditedProjektName(parts.join(' — '));
            }

            // Sonderfall Teilungserklärung: statt normalem Feld-Review
            // eine Einheitenliste mit Multi-Objekt-Anlage-Flow.
            if (selectedTypeId === 'teilungserklaerung' && Array.isArray(env.fields?.einheiten)) {
              setTeEinheiten(env.fields.einheiten.map((e, i) => ({
                ...e,
                _id: `te-${i}`,               // lokale UI-ID
                _accepted: true,               // Default: alle übernehmen
              })));
              setPhase('te_review');
            } else if (autoApply) {
              // Auto-Apply: Alle Felder und Beteiligte direkt übernehmen
              const scalars = getScalarFields(env.fields, selectedTypeId);
              const betArr = Array.isArray(env.fields.beteiligte) ? env.fields.beteiligte : [];
              try {
                const body = {
                  dokument_id: data.dokument_id,
                  applied_fields: scalars,
                  custom_values: {},
                  applied_beteiligte_idx: betArr.map((_, i) => i),
                  force_overwrite: [],
                  document_title: suggested,
                };
                const applyRes = await fetch(`${workerUrl}/api/apply-extraction`, {
                  method: 'POST',
                  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
                  body: JSON.stringify(body),
                });
                if (applyRes.ok) {
                  const result = await applyRes.json();
                  showToast(`${result.written_count || scalars.length} Felder aus ${dokumenttypLabel(selectedTypeId)} übernommen`, 'success');
                }
              } catch (e) {
                console.warn('[AutoApply] Fehler:', e.message);
              }
              // Direkt weiter zum nächsten Dokument (oder schließen)
              // Projekt umbenennen wenn Quick-Create (nur beim 1. Dokument)
              if (isQuickCreate && editedProjektName.trim() && editedProjektName !== projektName && onRenameProjekt) {
                try { await onRenameProjekt(editedProjektName.trim()); } catch {}
              }
              if (onApplied) { try { await onApplied(); } catch {} }
              onClose();
            } else {
              setPhase('review');
            }
          } else if (eventName === 'error') {
            throw new Error(data.message || 'Unbekannter Extraktionsfehler');
          }
        }
      }
    } catch (err) {
      setProgressError(err.message || String(err));
    }
  };

  // Auto-Start wenn prefillFile übergeben wurde (KI-Flow aus NeuerAuftragModal)
  const autoStarted = useRef(false);
  useEffect(() => {
    if (prefillFile && !noAutoStart && !autoStarted.current && phase === 'form') {
      autoStarted.current = true;
      // Einen Tick warten, damit der Modal renderst ist und state stabil
      setTimeout(() => { handleUpload(); }, 50);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [prefillFile]);

  const toggleField = (key) => {
    setAcceptedFields(prev => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  };

  const toggleBeteiligter = (idx) => {
    setAcceptedBeteiligteIdx(prev => {
      const next = new Set(prev);
      if (next.has(idx)) next.delete(idx);
      else next.add(idx);
      return next;
    });
  };

  // TE-Multi-Objekt-Apply: Für jede akzeptierte Einheit ein bewertungsobjekt
  // anlegen, Fortschritt pro Einheit melden, dann das Dokument als applied
  // markieren. Fehler pro Einheit werden gesammelt, der Flow bricht aber
  // nicht ab — Teilerfolg ist besser als Komplett-Rollback.
  const handleTeApply = async () => {
    const targetGutachtenId = aktiverGutachtenId;
    if (!targetGutachtenId) {
      setApplyError('Kein aktives Gutachten — Einheiten können nicht angelegt werden.');
      return;
    }
    const accepted = teEinheiten.filter(e => e._accepted);
    if (accepted.length === 0) {
      setApplyError('Mindestens eine Einheit auswählen.');
      return;
    }

    setApplying(true);
    setApplyError(null);
    setTeInsertProgress({ done: 0, total: accepted.length, errors: [] });

    let done = 0;
    const errors = [];
    for (const einheit of accepted) {
      try {
        await apiInsertRow('bewertungsobjekte', {
          gutachten_id: targetGutachtenId,
          bezeichnung: einheit.bezeichnung || `Einheit ${einheit.einheit_nr || ''}`.trim(),
          objekttyp: einheit.objekttyp || null,
          wohnflaeche: (einheit.wohnflaeche != null && einheit.wohnflaeche !== '')
            ? Number(einheit.wohnflaeche) : null,
          mea: einheit.mea || null,
          sondereigentumseinheit: einheit.sondereigentumseinheit || null,
        }, session, workerUrl);
        done++;
      } catch (err) {
        errors.push({ einheit: einheit.bezeichnung || einheit.einheit_nr, message: err.message || String(err) });
      }
      setTeInsertProgress({ done, total: accepted.length, errors: [...errors] });
    }

    // Dokument als applied markieren (kein applyRouting → Worker setzt nur review_status)
    try {
      await fetch(`${workerUrl}/api/apply-extraction`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${session.access_token}`,
        },
        body: JSON.stringify({
          dokument_id: dokumentId,
          applied_fields: [],
          custom_values: {},
          applied_beteiligte_idx: [],
          force_overwrite: [],
          document_title: editedTitle || null,
        }),
      });
    } catch (e) {
      // Nicht-kritisch: die Objekte sind angelegt, nur der review_status
      // bleibt möglicherweise auf 'pending'
      console.warn('[te-apply] dokument-apply fehlgeschlagen:', e);
    }

    setApplying(false);
    if (errors.length === accepted.length) {
      // Kein einziger Insert erfolgreich
      setApplyError(`Alle ${accepted.length} Einheiten-Anlagen fehlgeschlagen. Letzter Fehler: ${errors[errors.length - 1].message}`);
      setTeInsertProgress(null);
      return;
    }
    setAppliedSummary({
      written_count: done,
      writes: [{ target: 'bewertungsobjekte', count: done }],
      warnings: errors.length > 0 ? errors.map(e => `${e.einheit}: ${e.message}`) : [],
    });
    setPhase('applied');
  };

  // WEG Auto-Apply: Erstellt Bewertungsobjekte aus versteigerungsobjekte-Array
  const handleWegAutoApply = async () => {
    const vos = extractedFields?.fields?.versteigerungsobjekte;
    if (!vos || !Array.isArray(vos) || vos.length < 2) return;
    if (!aktiverGutachtenId) {
      setWegError('Kein aktives Gutachten vorhanden.');
      return;
    }

    setWegCreating(true);
    setWegError(null);

    try {
      // Erstes Objekt: bestehendes Bewertungsobjekt mit Daten der ersten Einheit aktualisieren
      // Restliche Einheiten: neue Bewertungsobjekte anlegen
      const existingObjs = await (async () => {
        const sb = await initSupabase();
        const { data } = await sb
          .from('bewertungsobjekte')
          .select('id, bezeichnung')
          .eq('gutachten_id', aktiverGutachtenId)
          .order('sort_order', { ascending: true });
        return data || [];
      })();

      let created = 0;
      let updated = 0;

      for (let i = 0; i < vos.length; i++) {
        const unit = vos[i];
        const payload = {
          bezeichnung: unit.sondereigentum_art || `Einheit ${unit.lfd_nr || (i + 1)}`,
          objekttyp: unit.objekttyp || null,
          mea: unit.mea || null,
          sondereigentumseinheit: unit.se_nr || null,
          grundbuchblatt: unit.grundbuchblatt || null,
        };

        if (i === 0 && existingObjs.length > 0) {
          // Erstes vorhandenes Objekt aktualisieren
          await apiPatchRow('bewertungsobjekte', existingObjs[0].id, payload, session, workerUrl);
          updated++;
        } else {
          // Neues Objekt anlegen
          await apiInsertRow('bewertungsobjekte', {
            gutachten_id: aktiverGutachtenId,
            sort_order: i,
            ...payload,
          }, session, workerUrl);
          created++;
        }
      }

      setWegCreated(true);
      showToast(`${vos.length} Bewertungsobjekte angelegt`, 'success');
    } catch (err) {
      setWegError(err.message || 'Fehler beim Anlegen der Bewertungsobjekte');
    } finally {
      setWegCreating(false);
    }
  };

  const handleApply = async () => {
    setApplying(true);
    setApplyError(null);
    try {
      // Editierte Beteiligte und Eigentümer in custom_values einbetten,
      // damit der Worker die bearbeiteten Werte verwendet statt der Original-
      // Extraktion. Werden nur aufgenommen, wenn tatsächlich editiert wurde
      // (sonst bleibt das Original im fields-Envelope).
      const mergedCustomValues = { ...customValues };
      if (editedBeteiligte.length > 0) mergedCustomValues.beteiligte = editedBeteiligte;
      if (editedEigentuemer.length > 0) mergedCustomValues.eigentuemer = editedEigentuemer;

      const body = {
        dokument_id: dokumentId,
        applied_fields: Array.from(acceptedFields),
        custom_values: mergedCustomValues,
        applied_beteiligte_idx: Array.from(acceptedBeteiligteIdx),
        force_overwrite: forceOverwrite,
        document_title: editedTitle || null,
      };
      const res = await fetch(`${workerUrl}/api/apply-extraction`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${session.access_token}`,
        },
        body: JSON.stringify(body),
      });
      if (!res.ok) {
        const errBody = await res.json().catch(() => ({}));
        throw new Error(errBody.error || `HTTP ${res.status}`);
      }
      const result = await res.json();
      // Konflikte: bereits gefüllte Felder wurden nicht überschrieben.
      // Wir wechseln in eine Konflikt-Review-Phase, statt direkt 'applied'.
      if (Array.isArray(result.conflicts) && result.conflicts.length > 0) {
        setConflicts(result.conflicts);
        setAppliedSummary(result);
        setPhase('conflicts');
      } else {
        setAppliedSummary(result);
        setPhase('applied');
      }
    } catch (err) {
      setApplyError(err.message || String(err));
    } finally {
      setApplying(false);
    }
  };

  const handleFinalClose = async () => {
    // Quick-Create: Projektname aktualisieren wenn geändert
    if (isQuickCreate && editedProjektName.trim() && editedProjektName !== projektName && onRenameProjekt) {
      try { await onRenameProjekt(editedProjektName.trim()); } catch (e) {
        console.warn('[UploadModal] Rename failed:', e.message);
      }
    }
    // Refresh MUSS vor onClose abgeschlossen sein
    if (onApplied) {
      try { await onApplied(); } catch {}
    }
    onClose();
  };

  // Konflikt-Workflow: Ein Konflikt bedeutet, dass der DB-Wert bereits
  // gefüllt war und ohne force_overwrite nicht überschrieben wurde.
  // Der User wählt pro Konflikt, ob der extrahierte Wert den bestehenden
  // ersetzen soll, dann wird handleApply erneut aufgerufen mit dem
  // aufgebauten force_overwrite-Array.
  const [forceChoices, setForceChoices] = useState({});  // { `${target}:${row_id}:${field}`: boolean }

  const conflictKey = (c) => `${c.target}:${c.row_id}:${c.field}`;

  const handleConflictResolve = async () => {
    // Die markierten Force-Einträge im State bauen wir ins force_overwrite-
    // Array um, das der Worker erwartet.
    const markedForceOverwrite = Object.entries(forceChoices)
      .filter(([, v]) => v === true)
      .map(([key]) => {
        const [target, row_id, field] = key.split(':');
        return { target, row_id, field };
      });
    setForceOverwrite(markedForceOverwrite);

    setApplying(true);
    setApplyError(null);
    try {
      const mergedCustomValues = { ...customValues };
      if (editedBeteiligte.length > 0) mergedCustomValues.beteiligte = editedBeteiligte;
      if (editedEigentuemer.length > 0) mergedCustomValues.eigentuemer = editedEigentuemer;

      const body = {
        dokument_id: dokumentId,
        applied_fields: Array.from(acceptedFields),
        custom_values: mergedCustomValues,
        applied_beteiligte_idx: Array.from(acceptedBeteiligteIdx),
        force_overwrite: markedForceOverwrite,
        document_title: editedTitle || null,
      };
      const res = await fetch(`${workerUrl}/api/apply-extraction`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${session.access_token}`,
        },
        body: JSON.stringify(body),
      });
      if (!res.ok) {
        const errBody = await res.json().catch(() => ({}));
        throw new Error(errBody.error || `HTTP ${res.status}`);
      }
      const result = await res.json();
      setAppliedSummary(result);
      setConflicts([]);
      setPhase('applied');
    } catch (err) {
      setApplyError(err.message || String(err));
    } finally {
      setApplying(false);
    }
  };

  // ══════ RENDERING ══════

  if (phase === 'conflicts') {
    const anyForceSelected = Object.values(forceChoices).some(Boolean);
    return (
      <div className="modal-backdrop">
        <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 720 }}>
          <div className="modal-header">
            <div>
              <div className="modal-title" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                Bestehende Werte überschreiben?
                <QueuePositionBadge queueContext={queueContext} />
              </div>
              <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 }}>
                {conflicts.length === 1
                  ? '1 Feld ist bereits gefüllt.'
                  : `${conflicts.length} Felder sind bereits gefüllt.`}
                {' '}Standardmäßig bleiben sie bestehen — markiere die Felder, die du mit dem neuen Wert ersetzen willst.
              </div>
            </div>
            <button className="modal-close" onClick={onClose} disabled={applying}>×</button>
          </div>
          <div className="modal-body" style={{ padding: 'var(--space-4) var(--space-5)' }}>
            {appliedSummary?.warnings?.length > 0 && (
              <div style={{
                background: 'var(--warning-bg)', color: 'var(--warning)',
                padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
                marginBottom: 'var(--space-4)', fontSize: 13,
              }}>
                <strong>Hinweis:</strong>{' '}
                {appliedSummary.warnings.map((w, i) => (
                  <span key={i}>{typeof w === 'string' ? w : `${w.target}: ${w.error}`}{i < appliedSummary.warnings.length - 1 ? ' · ' : ''}</span>
                ))}
              </div>
            )}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {conflicts.map((c) => {
                const k = conflictKey(c);
                const forceThis = forceChoices[k] === true;
                const targetLabel = TARGET_LABELS[c.target] || c.target;
                const fieldLabel = FELD_LABELS[c.field] || c.field;
                return (
                  <div
                    key={k}
                    style={{
                      padding: 'var(--space-3)',
                      border: `1px solid ${forceThis ? 'var(--warning)' : 'var(--border-light)'}`,
                      borderRadius: 'var(--radius-md)',
                      background: forceThis ? 'var(--warning-bg)' : 'transparent',
                    }}
                  >
                    <div style={{
                      fontSize: 13, fontWeight: 600, color: 'var(--text-primary)',
                      marginBottom: 'var(--space-2)',
                    }}>
                      {fieldLabel}
                      <span style={{
                        fontSize: 11, fontWeight: 400, color: 'var(--text-tertiary)',
                        marginLeft: 6, textTransform: 'uppercase', letterSpacing: '0.06em',
                      }}>
                        in {targetLabel}
                      </span>
                    </div>
                    <div style={{
                      display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-3)',
                      fontSize: 13, marginBottom: 'var(--space-3)',
                    }}>
                      <div style={{
                        padding: '8px 10px', borderRadius: 'var(--radius-sm)',
                        background: forceThis ? 'rgba(220, 38, 38, 0.05)' : 'var(--surface-light)',
                        border: forceThis ? '1px solid rgba(220, 38, 38, 0.15)' : '1px solid var(--border-light)',
                      }}>
                        <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 2, display: 'flex', alignItems: 'center', gap: 4 }}>
                          <span style={{ width: 6, height: 6, borderRadius: 99, background: forceThis ? 'var(--danger)' : 'var(--text-tertiary)', display: 'inline-block' }}></span>
                          Bestehender Wert
                        </div>
                        <div style={{ fontWeight: forceThis ? 400 : 600, textDecoration: forceThis ? 'line-through' : 'none', color: forceThis ? 'var(--text-tertiary)' : 'var(--text-primary)' }}>
                          {formatFieldValue(c.field, c.existing)}
                        </div>
                      </div>
                      <div style={{
                        padding: '8px 10px', borderRadius: 'var(--radius-sm)',
                        background: forceThis ? 'rgba(22, 163, 74, 0.05)' : 'var(--surface-light)',
                        border: forceThis ? '1px solid rgba(22, 163, 74, 0.2)' : '1px solid var(--border-light)',
                      }}>
                        <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 2, display: 'flex', alignItems: 'center', gap: 4 }}>
                          <span style={{ width: 6, height: 6, borderRadius: 99, background: forceThis ? 'var(--success)' : 'var(--text-tertiary)', display: 'inline-block' }}></span>
                          Neuer Wert (aus Dokument)
                        </div>
                        <div style={{ fontWeight: forceThis ? 600 : 400, color: forceThis ? 'var(--success)' : 'var(--text-secondary)' }}>
                          {formatFieldValue(c.field, c.incoming)}
                        </div>
                      </div>
                    </div>
                    <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
                      <button
                        type="button"
                        onClick={() => setForceChoices(fc => ({ ...fc, [k]: false }))}
                        style={conflictChoiceBtn(!forceThis, 'var(--text-secondary)')}
                      >Bestehenden behalten</button>
                      <button
                        type="button"
                        onClick={() => setForceChoices(fc => ({ ...fc, [k]: true }))}
                        style={conflictChoiceBtn(forceThis, 'var(--warning)')}
                      >Mit neuem überschreiben</button>
                    </div>
                  </div>
                );
              })}
            </div>
            {applyError && (
              <div style={{
                marginTop: 'var(--space-3)',
                background: 'var(--danger-bg)', color: 'var(--danger)',
                padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
                fontSize: 13,
              }}>
                <strong>Fehler:</strong> {applyError}
              </div>
            )}
          </div>
          <div className="modal-footer">
            <button
              className="btn btn-ghost"
              onClick={onClose}
              disabled={applying}
            >
              Abbrechen
            </button>
            {anyForceSelected ? (
              <button
                className="btn btn-primary"
                onClick={handleConflictResolve}
                disabled={applying}
                style={{ background: 'var(--warning)' }}
              >
                {applying
                  ? 'Wende an …'
                  : `${Object.values(forceChoices).filter(Boolean).length} Wert${Object.values(forceChoices).filter(Boolean).length === 1 ? '' : 'e'} überschreiben`}
              </button>
            ) : (
              <button
                className="btn btn-primary"
                onClick={async () => {
                  if (onApplied) {
                    try { await onApplied(); } catch {}
                  }
                  onClose();
                }}
                disabled={applying}
              >
                Schließen, alles bleibt bestehen
              </button>
            )}
          </div>
        </div>
      </div>
    );
  }

  // ══════ APPLIED-PHASE ══════

  if (phase === 'applied') {
    const hasMoreInQueue = queueContext && queueContext.position < queueContext.total;
    const nextLabel = queueContext?.nextTypLabel;
    return (
      <div className="modal-backdrop" onClick={handleFinalClose}>
        <div className="modal" onClick={e => e.stopPropagation()}>
          <div className="modal-header">
            <div className="modal-title" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
              Fertig
              <QueuePositionBadge queueContext={queueContext} />
            </div>
            <button className="modal-close" onClick={handleFinalClose}>×</button>
          </div>
          <div className="modal-body">
            <div style={{ textAlign: 'center', padding: 'var(--space-5) 0' }}>
              <div style={{ fontSize: 48, color: 'var(--success)' }}>✓</div>
              <div style={{ fontSize: 17, fontWeight: 600, marginTop: 'var(--space-2)' }}>
                Übernommen
              </div>
              <div style={{ color: 'var(--text-secondary)', fontSize: 14, marginTop: 4 }}>
                {appliedSummary?.written_count || 0} Felder wurden aktualisiert.
              </div>
              {appliedSummary?.writes?.length > 0 && (
                <div style={{
                  marginTop: 'var(--space-4)',
                  fontSize: 12, color: 'var(--text-tertiary)',
                  textAlign: 'left', padding: 'var(--space-3)',
                  background: 'var(--surface-light)', borderRadius: 'var(--radius-md)',
                }}>
                  {appliedSummary.writes.map((w, i) => (
                    <div key={i}>
                      → {w.target}: {w.count ? `${w.count} Einträge` : w.fields?.join(', ')}
                    </div>
                  ))}
                </div>
              )}
              {appliedSummary?.warnings?.length > 0 && (
                <div style={{
                  marginTop: 'var(--space-3)',
                  padding: 'var(--space-3)',
                  background: 'var(--warning-bg, #FFF8E1)',
                  color: 'var(--warning, #C47500)',
                  borderRadius: 'var(--radius-md)',
                  fontSize: 12, textAlign: 'left',
                }}>
                  <strong>Hinweis:</strong>{' '}
                  {appliedSummary.warnings.map((w, i) => (
                    <div key={i}>{typeof w === 'string' ? w : `${w.target}: ${w.error}`}</div>
                  ))}
                </div>
              )}
              {hasMoreInQueue && nextLabel && (
                <div style={{
                  marginTop: 'var(--space-4)',
                  padding: 'var(--space-3)',
                  background: 'var(--surface-blue, #EEF3F8)',
                  border: '1px solid var(--vl-blue-light, #1E5A8E)',
                  borderRadius: 'var(--radius-md)',
                  fontSize: 13, color: 'var(--vl-blue, #003B71)',
                }}>
                  Als nächstes: <strong>{nextLabel}</strong>
                </div>
              )}

              {/* WEG Auto-Apply: Versteigerungsobjekte → Bewertungsobjekte */}
              {(() => {
                const vos = extractedFields?.fields?.versteigerungsobjekte;
                if (!vos || !Array.isArray(vos) || vos.length < 2) return null;
                return (
                  <div style={{
                    marginTop: 'var(--space-4)',
                    padding: 'var(--space-4)',
                    background: 'var(--surface-light)',
                    border: '1px solid var(--vl-orange)',
                    borderRadius: 'var(--radius-md)',
                    textAlign: 'left',
                  }}>
                    <div style={{ fontSize: 14, fontWeight: 700, marginBottom: 8, color: 'var(--text-primary)' }}>
                      {vos.length} Versteigerungsobjekte erkannt
                    </div>
                    <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 12 }}>
                      Der Beschluss enthält mehrere WEG-Einheiten. Diese können automatisch als Bewertungsobjekte angelegt werden.
                    </div>
                    <table style={{ width: '100%', fontSize: 12, borderCollapse: 'collapse', marginBottom: 12 }}>
                      <thead>
                        <tr style={{ borderBottom: '1px solid var(--border-light)' }}>
                          <th style={{ textAlign: 'left', padding: '4px 8px', fontWeight: 600, color: 'var(--text-tertiary)' }}>Nr.</th>
                          <th style={{ textAlign: 'left', padding: '4px 8px', fontWeight: 600, color: 'var(--text-tertiary)' }}>Art</th>
                          <th style={{ textAlign: 'left', padding: '4px 8px', fontWeight: 600, color: 'var(--text-tertiary)' }}>MEA</th>
                          <th style={{ textAlign: 'left', padding: '4px 8px', fontWeight: 600, color: 'var(--text-tertiary)' }}>SE-Nr.</th>
                          <th style={{ textAlign: 'left', padding: '4px 8px', fontWeight: 600, color: 'var(--text-tertiary)' }}>Blatt</th>
                        </tr>
                      </thead>
                      <tbody>
                        {vos.map((v, i) => (
                          <tr key={i} style={{ borderBottom: '1px solid var(--border-light)' }}>
                            <td style={{ padding: '4px 8px' }}>{v.lfd_nr || (i + 1)}</td>
                            <td style={{ padding: '4px 8px', fontWeight: 500 }}>{v.sondereigentum_art || v.objekttyp || '—'}</td>
                            <td style={{ padding: '4px 8px' }}>{v.mea || '—'}</td>
                            <td style={{ padding: '4px 8px' }}>{v.se_nr || '—'}</td>
                            <td style={{ padding: '4px 8px' }}>{v.grundbuchblatt || '—'}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                    {wegCreated ? (
                      <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--success)' }}>
                        ✓ {vos.length} Bewertungsobjekte angelegt
                      </div>
                    ) : (
                      <button
                        className="btn btn-primary btn-sm"
                        onClick={handleWegAutoApply}
                        disabled={wegCreating}
                        style={{ width: '100%' }}
                      >
                        {wegCreating ? 'Lege an...' : `${vos.length} Bewertungsobjekte anlegen`}
                      </button>
                    )}
                    {wegError && (
                      <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 8 }}>{wegError}</div>
                    )}
                  </div>
                );
              })()}
            </div>
          </div>
          <div className="modal-footer">
            <button className="btn btn-primary" onClick={handleFinalClose}>
              {hasMoreInQueue
                ? (nextLabel ? `Weiter zu ${nextLabel}` : 'Weiter zum nächsten Dokument')
                : 'Schließen'}
            </button>
          </div>
        </div>
      </div>
    );
  }

  if (phase === 'progress') {
    const STEPS = {
      uploading: 'Datei wird hochgeladen…',
      extracting: 'KI liest das Dokument…',
      persisting: 'Ergebnis wird gespeichert…',
    };
    return (
      <div className="modal-backdrop">
        <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 480 }}>
          <div className="modal-header">
            <div className="modal-title" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
              Dokument wird verarbeitet
              <QueuePositionBadge queueContext={queueContext} />
            </div>
          </div>
          <div className="modal-body" style={{ padding: 'var(--space-6)' }}>
            {progressError ? (
              <div>
                <div style={{
                  background: 'var(--danger-bg)', color: 'var(--danger)',
                  padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
                  fontSize: 13, marginBottom: 'var(--space-4)',
                }}>
                  <strong>Fehler:</strong> {progressError}
                </div>
                <button className="btn btn-secondary" onClick={() => setPhase('form')}>
                  Zurück zum Upload
                </button>
              </div>
            ) : (
              <>
                <div style={{ fontSize: 15, fontWeight: 500, marginBottom: 'var(--space-3)' }}>
                  {STEPS[progressStep] || 'Verarbeitung läuft…'}
                </div>
                <div className="progress-bar" style={{ height: 8 }}>
                  <div className="progress-bar-fill" style={{ width: `${progressPct}%` }}></div>
                </div>
                <div style={{
                  marginTop: 'var(--space-3)', fontSize: 12,
                  color: 'var(--text-tertiary)', textAlign: 'center',
                }}>
                  Das dauert meist 10-30 Sekunden. Bitte nicht schließen.
                </div>
              </>
            )}
          </div>
        </div>
      </div>
    );
  }

  // Teilungserklärung-Sonderflow: Einheitenliste zur Multi-Objekt-Anlage
  if (phase === 'te_review' && extractedFields) {
    const acceptedCount = teEinheiten.filter(e => e._accepted).length;
    const allAccepted = teEinheiten.length > 0 && acceptedCount === teEinheiten.length;
    const toggleAll = () => {
      const next = !allAccepted;
      setTeEinheiten(list => list.map(e => ({ ...e, _accepted: next })));
    };
    const updateEinheit = (id, patch) => {
      setTeEinheiten(list => list.map(e => e._id === id ? { ...e, ...patch } : e));
    };

    return (
      <div className="modal-backdrop">
        <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 880 }}>
          <div className="modal-header">
            <div>
              <div className="modal-title" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                Einheiten aus Teilungserklärung anlegen
                <QueuePositionBadge queueContext={queueContext} />
              </div>
              <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 }}>
                {teEinheiten.length} Einheit{teEinheiten.length !== 1 ? 'en' : ''} erkannt. Auswählen, ggf. anpassen, dann als Bewertungsobjekte im aktuellen Gutachten anlegen.
              </div>
            </div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
              {dokumentId && (
                <button
                  type="button"
                  className="btn btn-ghost btn-sm"
                  onClick={() => oeffneDokument(dokumentId, session, workerUrl)}
                  style={{ fontSize: 12 }}
                  title="Original-PDF in neuem Tab öffnen"
                >
                  Original ansehen
                </button>
              )}
              <button className="modal-close" onClick={onClose} disabled={applying}>×</button>
            </div>
          </div>

          <div className="modal-body" style={{ padding: 0 }}>
            {isMock && (
              <div style={{
                background: 'var(--warning-bg)', color: 'var(--warning)',
                padding: 'var(--space-3) var(--space-5)', fontSize: 13,
                borderBottom: '1px solid var(--border-light)',
              }}>
                <strong>Dev-Modus:</strong> Diese Extraktion zeigt Mock-Daten, weil kein ANTHROPIC_API_KEY konfiguriert ist.
              </div>
            )}

            {/* Dokument-Titel */}
            <div style={{
              padding: 'var(--space-3) var(--space-5)',
              borderBottom: '1px solid var(--border-light)',
              background: 'var(--surface-light)',
            }}>
              <div style={{
                fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
                letterSpacing: '0.08em', color: 'var(--text-tertiary)',
                marginBottom: 4,
              }}>
                Dokument-Titel
              </div>
              <input
                type="text"
                value={editedTitle}
                onChange={(e) => setEditedTitle(e.target.value)}
                placeholder="Titel für dieses Dokument"
                style={{
                  width: '100%',
                  padding: '6px 10px',
                  fontSize: 14, fontWeight: 500,
                  fontFamily: 'inherit',
                  color: 'var(--text-primary)',
                  background: 'var(--surface)',
                  border: '1px solid var(--border-light)',
                  borderRadius: 'var(--radius-sm)',
                }}
              />
            </div>

            {/* Toolbar */}
            <div style={{
              display: 'flex', justifyContent: 'space-between',
              padding: 'var(--space-3) var(--space-5)',
              borderBottom: '1px solid var(--border-light)',
              alignItems: 'center',
            }}>
              <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
                {acceptedCount} von {teEinheiten.length} Einheiten ausgewählt
              </div>
              <button className="btn btn-ghost btn-sm" onClick={toggleAll}>
                {allAccepted ? 'Alle abwählen' : 'Alle auswählen'}
              </button>
            </div>

            {/* Einheiten-Tabelle */}
            <div style={{ padding: 'var(--space-3) var(--space-5)', maxHeight: '50vh', overflowY: 'auto' }}>
              <div style={{
                display: 'grid',
                gridTemplateColumns: '28px 1fr 140px 100px 100px',
                gap: 'var(--space-2)',
                padding: '6px 10px',
                fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
                letterSpacing: '0.06em', color: 'var(--text-tertiary)',
                borderBottom: '1px solid var(--border-light)',
              }}>
                <span></span>
                <span>Bezeichnung</span>
                <span>Typ</span>
                <span>Wohnfläche</span>
                <span>MEA</span>
              </div>
              {teEinheiten.map(einheit => (
                <div
                  key={einheit._id}
                  style={{
                    display: 'grid',
                    gridTemplateColumns: '28px 1fr 140px 100px 100px',
                    gap: 'var(--space-2)',
                    padding: '8px 10px',
                    alignItems: 'center',
                    borderBottom: '1px solid var(--border-light)',
                    background: einheit._accepted ? 'var(--success-bg)' : 'transparent',
                  }}
                >
                  <input
                    type="checkbox"
                    checked={einheit._accepted}
                    onChange={() => updateEinheit(einheit._id, { _accepted: !einheit._accepted })}
                    style={{ accentColor: 'var(--success)', cursor: 'pointer' }}
                  />
                  <div style={{ fontSize: 13 }}>
                    <EditableField
                      value={einheit.bezeichnung}
                      onSave={async (v) => updateEinheit(einheit._id, { bezeichnung: v })}
                    />
                    {einheit.lage && (
                      <div style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                        {einheit.lage}
                        {einheit.einheit_nr && ` · Nr. ${einheit.einheit_nr}`}
                      </div>
                    )}
                  </div>
                  <div style={{ fontSize: 13 }}>
                    <EditableField
                      value={einheit.objekttyp}
                      type="select"
                      options={OBJEKTTYP_OPTIONS}
                      display={OBJEKTTYP_OPTIONS.find(o => o.value === einheit.objekttyp)?.label || einheit.objekttyp}
                      onSave={async (v) => updateEinheit(einheit._id, { objekttyp: v })}
                    />
                  </div>
                  <div style={{ fontSize: 13 }}>
                    <EditableField
                      value={einheit.wohnflaeche}
                      type="number"
                      display={einheit.wohnflaeche ? `${einheit.wohnflaeche} m²` : null}
                      onSave={async (v) => updateEinheit(einheit._id, { wohnflaeche: v })}
                    />
                  </div>
                  <div style={{ fontSize: 13 }}>
                    <EditableField
                      value={einheit.mea}
                      onSave={async (v) => updateEinheit(einheit._id, { mea: v })}
                    />
                  </div>
                </div>
              ))}
            </div>

            {/* Fortschritt während Insert */}
            {teInsertProgress && (
              <div style={{
                padding: 'var(--space-3) var(--space-5)',
                borderTop: '1px solid var(--border-light)',
                background: 'var(--surface-light)',
              }}>
                <div style={{ fontSize: 13, marginBottom: 'var(--space-2)' }}>
                  Legt Bewertungsobjekte an: {teInsertProgress.done} von {teInsertProgress.total}
                  {teInsertProgress.errors.length > 0 && (
                    <span style={{ color: 'var(--danger)', marginLeft: 8 }}>
                      ({teInsertProgress.errors.length} Fehler)
                    </span>
                  )}
                </div>
                <div style={{
                  height: 4, background: 'var(--border-light)',
                  borderRadius: 2, overflow: 'hidden',
                }}>
                  <div style={{
                    height: '100%',
                    width: `${(teInsertProgress.done / teInsertProgress.total) * 100}%`,
                    background: 'var(--success)',
                    transition: 'width 0.2s',
                  }}></div>
                </div>
              </div>
            )}

            {applyError && (
              <div style={{
                margin: 'var(--space-4) var(--space-5)',
                background: 'var(--danger-bg)', color: 'var(--danger)',
                padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
                fontSize: 13,
              }}>
                <strong>Fehler:</strong> {applyError}
              </div>
            )}
          </div>

          <div className="modal-footer">
            <button className="btn btn-ghost" onClick={onClose} disabled={applying}>
              Abbrechen
            </button>
            <button
              className="btn btn-primary"
              onClick={handleTeApply}
              disabled={applying || acceptedCount === 0}
            >
              {applying
                ? `Lege an … (${teInsertProgress?.done || 0}/${teInsertProgress?.total || acceptedCount})`
                : `${acceptedCount} Bewertungsobjekt${acceptedCount === 1 ? '' : 'e'} anlegen`}
            </button>
          </div>
        </div>
      </div>
    );
  }

  if (phase === 'review' && extractedFields) {
    const fields = extractedFields.fields;
    const scalarKeys = getScalarFields(fields, selectedTypeId);
    const beteiligteArr = Array.isArray(fields.beteiligte) ? fields.beteiligte : [];
    const eigentuemerArr = Array.isArray(fields.eigentuemer) ? fields.eigentuemer : [];

    const allAccepted = scalarKeys.length > 0 && scalarKeys.every(k => acceptedFields.has(k))
                        && (beteiligteArr.length === 0 || beteiligteArr.every((_, i) => acceptedBeteiligteIdx.has(i)));
    const noneAccepted = acceptedFields.size === 0 && acceptedBeteiligteIdx.size === 0;

    const toggleAll = () => {
      if (allAccepted) {
        setAcceptedFields(new Set());
        setAcceptedBeteiligteIdx(new Set());
      } else {
        setAcceptedFields(new Set(scalarKeys));
        setAcceptedBeteiligteIdx(new Set(beteiligteArr.map((_, i) => i)));
      }
    };

    return (
      <div className="modal-backdrop">
        <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: pdfViewerUrl ? 1200 : 720, display: 'flex', flexDirection: 'column', maxHeight: '92vh' }}>
          <div className="modal-header">
            <div>
              <div className="modal-title" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                Extraktions-Ergebnis prüfen
                <QueuePositionBadge queueContext={queueContext} />
              </div>
              <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 }}>
                Wähle die Felder, die übernommen werden sollen.
                {queueContext && queueContext.position > 1 && (
                  <span style={{ color: 'var(--vl-orange)', fontWeight: 600 }}> Felder nicht vorausgewählt, da bereits Daten vorhanden.</span>
                )}
              </div>
            </div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
              {dokumentId && !pdfViewerUrl && (
                <button
                  type="button"
                  className="btn btn-ghost btn-sm"
                  onClick={() => oeffneDokument(dokumentId, session, workerUrl)}
                  style={{ fontSize: 12 }}
                  title="Original-PDF in neuem Tab öffnen"
                >
                  Original ansehen
                </button>
              )}
              {pdfViewerUrl && (
                <button
                  type="button"
                  className="btn btn-ghost btn-sm"
                  onClick={() => setPdfViewerUrl(null)}
                  style={{ fontSize: 12 }}
                >
                  PDF ausblenden
                </button>
              )}
              <button className="modal-close" onClick={onClose}>×</button>
            </div>
          </div>
          <div className="modal-body" style={{ padding: 0, display: pdfViewerUrl ? 'flex' : 'block', minHeight: pdfViewerUrl ? 600 : 'auto', overflow: 'auto', flex: 1 }}>
            {/* PDF-Viewer (linke Seite) */}
            {pdfViewerUrl && (
              <div style={{ flex: '0 0 50%', borderRight: '1px solid var(--border-light)', minHeight: 500 }}>
                <iframe
                  src={pdfViewerUrl}
                  style={{ width: '100%', height: '100%', border: 'none', minHeight: 600 }}
                  title="Original-Dokument"
                />
              </div>
            )}
            {/* Extraktions-Ergebnis (rechte Seite oder volle Breite) */}
            <div style={{ flex: 1, overflow: 'auto', maxHeight: pdfViewerUrl ? 700 : 'none' }}>
            {/* Quick-Create: Editierbarer Projektname */}
            {isQuickCreate && (
              <div style={{
                padding: 'var(--space-4) var(--space-5)',
                borderBottom: '1px solid var(--border-light)',
                background: 'var(--surface-light)',
              }}>
                <label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 6, color: 'var(--text-secondary)' }}>
                  Aktenzeichen (intern)
                </label>
                <input
                  type="text"
                  value={editedProjektName}
                  onChange={e => setEditedProjektName(e.target.value)}
                  placeholder="z.B. 145/25/LV"
                  style={{
                    width: '100%', padding: '10px 12px',
                    border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
                    fontSize: 14, fontFamily: 'inherit', outline: 'none',
                    background: 'var(--surface)', fontWeight: 600,
                  }}
                />
              </div>
            )}
            {isMock && (
              <div style={{
                background: 'var(--warning-bg)', color: 'var(--warning)',
                padding: 'var(--space-3) var(--space-5)', fontSize: 13,
                borderBottom: '1px solid var(--border-light)',
              }}>
                <strong>Dev-Modus:</strong> Diese Extraktion zeigt Mock-Daten, weil kein ANTHROPIC_API_KEY konfiguriert ist. Für echte Extraktion den Worker-Secret setzen.
              </div>
            )}

            <div style={{
              display: 'flex', justifyContent: 'space-between',
              padding: 'var(--space-3) var(--space-5)',
              borderBottom: '1px solid var(--border-light)',
              alignItems: 'center',
            }}>
              <div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
                {acceptedFields.size + acceptedBeteiligteIdx.size} von {scalarKeys.length + beteiligteArr.length} Einträgen akzeptiert
              </div>
              <button className="btn btn-ghost btn-sm" onClick={toggleAll}>
                {allAccepted ? 'Alle abwählen' : 'Alle auswählen'}
              </button>
            </div>

            {/* Dokumenten-Titel: KI-Vorschlag, vom User editierbar.
                Wird beim Apply in dokumente.titel geschrieben und taucht
                dann in der Unterlagen-Liste dieses Projekts auf. */}
            <div style={{
              padding: 'var(--space-3) var(--space-5)',
              borderBottom: '1px solid var(--border-light)',
              background: 'var(--surface-light)',
            }}>
              <div style={{
                fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
                letterSpacing: '0.08em', color: 'var(--text-tertiary)',
                marginBottom: 4,
              }}>
                Dokument-Titel
              </div>
              <input
                type="text"
                value={editedTitle}
                onChange={(e) => setEditedTitle(e.target.value)}
                placeholder="Titel für dieses Dokument"
                style={{
                  width: '100%',
                  padding: '6px 10px',
                  fontSize: 14, fontWeight: 500,
                  fontFamily: 'inherit',
                  color: 'var(--text-primary)',
                  background: 'var(--surface)',
                  border: '1px solid var(--border-light)',
                  borderRadius: 'var(--radius-sm)',
                }}
              />
              <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>
                {extractedFields?.fields?.titel_vorschlag
                  ? 'KI-Vorschlag — vor dem Übernehmen anpassbar.'
                  : 'Aus Dateiname abgeleitet — vor dem Übernehmen anpassbar.'}
              </div>
            </div>

            {/* Skalare Felder */}
            {scalarKeys.length > 0 && (
              <div style={{ padding: 'var(--space-4) var(--space-5)' }}>
                <div className="obj-section-title" style={{ marginBottom: 'var(--space-3)' }}>
                  Felder
                </div>
                <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 'var(--space-2)' }}>
                  Werte klicken zum Bearbeiten, bevor sie übernommen werden.
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
                  {scalarKeys.map(key => {
                    const accepted = acceptedFields.has(key);
                    const currentValue = customValues[key] !== undefined ? customValues[key] : fields[key];
                    return (
                      <div
                        key={key}
                        style={{
                          display: 'grid',
                          gridTemplateColumns: '28px 200px 1fr',
                          gap: 'var(--space-3)',
                          padding: 'var(--space-2) var(--space-3)',
                          alignItems: 'center',
                          borderRadius: 'var(--radius-sm)',
                          background: accepted ? 'var(--success-bg)' : 'transparent',
                          transition: 'background 0.12s',
                        }}
                      >
                        <input
                          type="checkbox"
                          checked={accepted}
                          onChange={() => toggleField(key)}
                          style={{ accentColor: 'var(--success)', cursor: 'pointer' }}
                          title={accepted ? 'Ausgewählt — wird übernommen' : 'Nicht ausgewählt'}
                        />
                        <span
                          style={{ fontSize: 13, color: 'var(--text-secondary)', cursor: 'pointer' }}
                          onClick={() => toggleField(key)}
                        >
                          {FELD_LABELS[key] || key}
                        </span>
                        <span style={{ fontSize: 14, fontWeight: 500 }}>
                          <EditableField
                            value={currentValue == null ? '' : String(currentValue)}
                            display={formatFieldValue(key, currentValue)}
                            type={key.startsWith('abgabe_') || key === 'auftragseingang' || key === 'beschlussdatum' ? 'date'
                                  : (key === 'kostenvorschuss' || key === 'ausfertigungen' || key === 'groesse_qm') ? 'number'
                                  : 'text'}
                            onSave={async (newVal) => {
                              setCustomValues(cv => ({ ...cv, [key]: newVal }));
                            }}
                          />
                        </span>
                      </div>
                    );
                  })}
                </div>
              </div>
            )}

            {/* Beteiligte */}
            {beteiligteArr.length > 0 && (
              <div style={{ padding: 'var(--space-4) var(--space-5)', borderTop: '1px solid var(--border-light)' }}>
                <div className="obj-section-title" style={{ marginBottom: 'var(--space-3)' }}>
                  Beteiligte ({beteiligteArr.length})
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                  {editedBeteiligte.map((b, i) => {
                    const accepted = acceptedBeteiligteIdx.has(i);
                    const updateBeteiligter = (patch) => {
                      setEditedBeteiligte(list =>
                        list.map((item, idx) => idx === i ? { ...item, ...patch } : item)
                      );
                    };
                    return (
                      <div
                        key={i}
                        style={{
                          display: 'grid',
                          gridTemplateColumns: '28px 1fr',
                          gap: 'var(--space-3)',
                          padding: 'var(--space-3)',
                          alignItems: 'start',
                          borderRadius: 'var(--radius-md)',
                          border: `1px solid ${accepted ? 'var(--success)' : 'var(--border-light)'}`,
                          background: accepted ? 'var(--success-bg)' : 'transparent',
                        }}
                      >
                        <input
                          type="checkbox"
                          checked={accepted}
                          onChange={() => toggleBeteiligter(i)}
                          style={{ marginTop: 2, accentColor: 'var(--success)', cursor: 'pointer' }}
                        />
                        <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
                          <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
                                        letterSpacing: '0.1em', color: 'var(--vl-orange-dark)' }}>
                            <EditableField
                              value={b.rolle}
                              onSave={async (v) => updateBeteiligter({ rolle: v })}
                            />
                          </div>
                          <div style={{ fontSize: 15, fontWeight: 600 }}>
                            <EditableField
                              value={b.name}
                              onSave={async (v) => updateBeteiligter({ name: v })}
                            />
                          </div>
                          <div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
                            <EditableField
                              value={b.anschrift}
                              placeholder="— Anschrift —"
                              onSave={async (v) => updateBeteiligter({ anschrift: v })}
                            />
                          </div>
                          {(b.anwalt_name || b.anwalt_anschrift) && (
                            <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                              RA: <EditableField
                                value={b.anwalt_name}
                                placeholder="— Anwalt —"
                                onSave={async (v) => updateBeteiligter({ anwalt_name: v })}
                              />
                            </div>
                          )}
                          <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                            Az.: <EditableField
                              value={b.aktenzeichen}
                              placeholder="—"
                              onSave={async (v) => updateBeteiligter({ aktenzeichen: v })}
                            />
                          </div>
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            )}

            {/* Eigentümer (Grundbuch) */}
            {eigentuemerArr.length > 0 && (
              <div style={{ padding: 'var(--space-4) var(--space-5)', borderTop: '1px solid var(--border-light)' }}>
                <label style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', cursor: 'pointer' }}>
                  <input
                    type="checkbox"
                    checked={acceptedFields.has('eigentuemer')}
                    onChange={() => toggleField('eigentuemer')}
                    style={{ accentColor: 'var(--success)' }}
                  />
                  <span className="obj-section-title" style={{ margin: 0 }}>
                    Eigentümer ({eigentuemerArr.length}) — als Beteiligte anlegen
                  </span>
                </label>
                <div style={{ marginLeft: 28, marginTop: 'var(--space-2)', fontSize: 14 }}>
                  {editedEigentuemer.map((e, i) => {
                    const updateE = (patch) => {
                      setEditedEigentuemer(list =>
                        list.map((item, idx) => idx === i ? { ...item, ...patch } : item)
                      );
                    };
                    return (
                      <div key={i} style={{ padding: '4px 0', color: 'var(--text-secondary)' }}>
                        <strong style={{ color: 'var(--text-primary)' }}>
                          <EditableField
                            value={e.name}
                            onSave={async (v) => updateE({ name: v })}
                          />
                        </strong>
                        {' ('}
                        <EditableField
                          value={e.anteil}
                          placeholder="Anteil"
                          onSave={async (v) => updateE({ anteil: v })}
                        />
                        {') · '}
                        <EditableField
                          value={e.anschrift}
                          placeholder="Anschrift"
                          onSave={async (v) => updateE({ anschrift: v })}
                        />
                      </div>
                    );
                  })}
                </div>
              </div>
            )}

            {applyError && (
              <div style={{
                margin: 'var(--space-4) var(--space-5)',
                background: 'var(--danger-bg)', color: 'var(--danger)',
                padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
                fontSize: 13,
              }}>
                <strong>Fehler:</strong> {applyError}
              </div>
            )}
            </div>{/* Ende rechte Seite / Content-Bereich */}
          </div>

          <div className="modal-footer">
            <button className="btn btn-ghost" onClick={onClose} disabled={applying}>
              Abbrechen
            </button>
            <button
              className="btn btn-primary"
              onClick={handleApply}
              disabled={applying || noneAccepted}
            >
              {applying ? 'Übernehme…' : `${acceptedFields.size + acceptedBeteiligteIdx.size} Einträge übernehmen`}
            </button>
          </div>
        </div>
      </div>
    );
  }

  // Phase: form (Default)
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <div className="modal-header">
          <div className="modal-title" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
            Dokument hinzufügen
            <QueuePositionBadge queueContext={queueContext} />
          </div>
          <button className="modal-close" onClick={onClose}>×</button>
        </div>
        <div className="modal-body">
          <div className="form-field">
            <label className="form-label">Dokumenttyp</label>
            <select
              className="form-select"
              value={selectedTypeId}
              onChange={e => setSelectedTypeId(e.target.value)}
            >
              {uploadTypes.map(group => (
                <optgroup key={group.group} label={group.group}>
                  {group.items.map(item => (
                    <option key={item.id} value={item.id}>
                      {item.label}{!item.extractable ? ' (noch keine KI-Extraktion)' : ''}
                    </option>
                  ))}
                </optgroup>
              ))}
            </select>
            <div className="form-help">
              {SCOPE_HELP_TEXT[scope]}
            </div>
            {!extractable && (
              <div style={{
                marginTop: 'var(--space-2)',
                fontSize: 12, color: 'var(--warning)',
                padding: 'var(--space-2) var(--space-3)',
                background: 'var(--warning-bg)',
                borderRadius: 'var(--radius-sm)',
              }}>
                Für diesen Dokumenttyp ist die KI-Extraktion in der aktuellen Version noch nicht aktiv. Die Datei wird nur abgelegt.
              </div>
            )}
          </div>

          {showObjektPicker && (
            <div className="form-field">
              <label className="form-label">Zu welchem Bewertungsobjekt?</label>
              <select
                className="form-select"
                value={selectedObjektIdx}
                onChange={e => setSelectedObjektIdx(parseInt(e.target.value, 10))}
              >
                {objekte.map((o, i) => (
                  <option key={i} value={i}>Objekt {i + 1} — {o.bezeichnung}</option>
                ))}
              </select>
            </div>
          )}

          {scope === 'objekt' && !showObjektPicker && objekte.length === 1 && (
            <div style={{
              padding: 'var(--space-2) var(--space-3)',
              background: 'var(--surface-blue)',
              borderRadius: 'var(--radius-sm)',
              fontSize: 12, color: 'var(--vl-blue)',
              marginBottom: 'var(--space-4)',
            }}>
              Zuordnung: <strong>{objekte[0].bezeichnung}</strong>
            </div>
          )}

          {scope === 'objekt' && objekte.length === 0 && (
            <div style={{
              padding: 'var(--space-2) var(--space-3)',
              background: 'var(--warning-bg)',
              borderRadius: 'var(--radius-sm)',
              fontSize: 12, color: 'var(--warning)',
              marginBottom: 'var(--space-4)',
            }}>
              Keine Bewertungsobjekte im aktuellen Gutachten. Dieses Dokument kann erst nach Anlage eines Objekts hochgeladen werden.
            </div>
          )}

          <div className="form-field">
            <label className="form-label">Datei</label>
            <input
              ref={fileInputRef}
              type="file"
              accept="application/pdf,image/jpeg,image/png,image/heic"
              style={{ display: 'none' }}
              onChange={handleFileSelect}
            />
            <div
              className="upload-zone"
              onClick={() => fileInputRef.current?.click()}
            >
              {file ? (
                <>
                  <div style={{ fontWeight: 500, color: 'var(--vl-blue)' }}>{file.name}</div>
                  <div style={{ fontSize: 12, marginTop: 4, color: 'var(--text-secondary)' }}>
                    {(file.size / 1024).toFixed(0)} KB · Andere Datei wählen…
                  </div>
                </>
              ) : (
                <>
                  <IconUpload size={32} stroke="var(--text-tertiary)" />
                  <div style={{ fontWeight: 500, color: 'var(--text-primary)', marginTop: 8 }}>
                    PDF, JPG oder PNG hierher ziehen
                  </div>
                  <div style={{ fontSize: 12, marginTop: 4 }}>
                    oder <span style={{ color: 'var(--vl-blue-light)' }}>durchsuchen</span>
                  </div>
                </>
              )}
            </div>
            {!file && typeof navigator !== 'undefined' && /Mobi|Android/i.test(navigator.userAgent) && (
              <button
                type="button"
                className="btn btn-ghost btn-sm"
                onClick={() => {
                  const inp = document.createElement('input');
                  inp.type = 'file';
                  inp.accept = 'image/*';
                  inp.capture = 'environment';
                  inp.onchange = (e) => { if (e.target.files?.[0]) handleFileSelect(e); };
                  inp.click();
                }}
                style={{ marginTop: 8, width: '100%', fontSize: 13 }}
              >
                Dokument abfotografieren
              </button>
            )}
          </div>

          {formError && (
            <div style={{
              background: 'var(--danger-bg)', color: 'var(--danger)',
              padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
              fontSize: 13,
            }}>
              {formError}
            </div>
          )}
        </div>
        <div className="modal-footer">
          <button className="btn btn-ghost" onClick={onClose}>Abbrechen</button>
          <button
            className="btn btn-primary"
            onClick={handleUpload}
            disabled={!file}
          >
            {extractable ? 'Hochladen und extrahieren' : 'Hochladen und ablegen'}
          </button>
        </div>
      </div>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════
// TOP BAR (mit dynamischem Breadcrumb)
// ══════════════════════════════════════════════════════════════════

const Breadcrumb = ({ view, projekt, aktiverGutachtenIdx, gutachtenCount, onNavigate }) => {
  if (view === 'projektliste') {
    return (
      <button className="breadcrumb-back" disabled>
        <IconChevronLeft size={16} /> Aufträge
      </button>
    );
  }

  if (view === 'kontakte') {
    return <span className="breadcrumb-current">Kontakte</span>;
  }

  if (view === 'vorlagen') {
    return <span className="breadcrumb-current">Vorlagen</span>;
  }

  if (view === 'meine_aufgaben') {
    return <span className="breadcrumb-current">Mein Bereich</span>;
  }

  // Fallback-Name während projekt noch lädt
  const projektName = projekt?.name || 'Lade…';

  if (view === 'dashboard') {
    return (
      <>
        <button className="breadcrumb-back" onClick={() => onNavigate('projektliste')}>
          <IconChevronLeft size={16} /> Aufträge
        </button>
        <span className="breadcrumb-separator">/</span>
        <span className="breadcrumb-current">{projektName}</span>
      </>
    );
  }

  if (view === 'auftrag') {
    return (
      <>
        <button className="breadcrumb-back" onClick={() => onNavigate('dashboard')}>
          <IconChevronLeft size={16} /> {projektName}
        </button>
        <span className="breadcrumb-separator">/</span>
        <span className="breadcrumb-current">Auftrag</span>
      </>
    );
  }

  if (view === 'gutachten') {
    const label = gutachtenCount > 1
      ? `Gutachten ${aktiverGutachtenIdx + 1} von ${gutachtenCount}`
      : 'Gutachten';
    return (
      <>
        <button className="breadcrumb-back" onClick={() => onNavigate('dashboard')}>
          <IconChevronLeft size={16} /> {projektName}
        </button>
        <span className="breadcrumb-separator">/</span>
        <span className="breadcrumb-current">{label}</span>
      </>
    );
  }

  return null;
};

// ══════════════════════════════════════════════════════════════════
// PROFIL-MODAL — Name bearbeiten + Passwort ändern
// ══════════════════════════════════════════════════════════════════
const ProfilModal = ({ userProfile, session, onClose, onUpdate }) => {
  const [name, setName] = useState(userProfile?.full_name || '');
  const [newPw, setNewPw] = useState('');
  const [newPwRepeat, setNewPwRepeat] = useState('');
  const [saving, setSaving] = useState(false);
  const [msg, setMsg] = useState(null);
  const [errMsg, setErrMsg] = useState(null);
  const [deleteConfirm, setDeleteConfirm] = useState(false);
  const [deleting, setDeleting] = useState(false);

  const WORKER_URL =
    window.location.hostname === 'sv.augenschein.app'      ? 'https://api-sv.augenschein.app'
    : window.location.hostname === 'custom.augenschein.app' ? 'https://api-custom.augenschein.app'
    : 'https://api-custom.augenschein.app'; // Dev: Prod-Worker

  const handleSaveName = async () => {
    if (!name.trim()) return;
    setSaving(true); setErrMsg(null); setMsg(null);
    try {
      const sb = await initSupabase();
      const { error } = await sb
        .from('user_profiles')
        .update({ full_name: name.trim() })
        .eq('id', session.user.id);
      if (error) throw error;
      _orgMembersCache = null;
      setMsg('Name gespeichert');
      if (onUpdate) onUpdate({ ...userProfile, full_name: name.trim() });
    } catch (e) {
      setErrMsg(e.message || 'Fehler beim Speichern');
    } finally { setSaving(false); }
  };

  const handleChangePw = async () => {
    if (newPw.length < 8) { setErrMsg('Mindestens 8 Zeichen'); return; }
    if (newPw !== newPwRepeat) { setErrMsg('Passwörter stimmen nicht überein'); return; }
    setSaving(true); setErrMsg(null); setMsg(null);
    try {
      const sb = await initSupabase();
      const { error } = await sb.auth.updateUser({ password: newPw });
      if (error) throw error;
      setMsg('Passwort geändert');
      setNewPw(''); setNewPwRepeat('');
    } catch (e) {
      setErrMsg(e.message || 'Fehler beim Ändern');
    } finally { setSaving(false); }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 440 }}>
        <div className="modal-header">
          <div className="modal-title">Profil</div>
          <button className="modal-close" onClick={onClose}>×</button>
        </div>
        <div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }}>
          <div>
            <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Name</div>
            <div style={{ display: 'flex', gap: 8 }}>
              <input className="form-input" value={name} onChange={e => setName(e.target.value)} style={{ flex: 1 }} />
              <button className="btn btn-primary btn-sm" onClick={handleSaveName} disabled={saving || !name.trim() || name.trim() === userProfile?.full_name}>
                Speichern
              </button>
            </div>
          </div>
          <div>
            <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.05em' }}>E-Mail</div>
            <div style={{ fontSize: 14, color: 'var(--text-tertiary)' }}>{session?.user?.email}</div>
          </div>
          <div>
            <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Passwort ändern</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              <input className="form-input" type="password" placeholder="Neues Passwort (min. 8 Zeichen)" value={newPw} onChange={e => setNewPw(e.target.value)} />
              <input className="form-input" type="password" placeholder="Passwort wiederholen" value={newPwRepeat} onChange={e => setNewPwRepeat(e.target.value)} />
              <button className="btn btn-secondary btn-sm" onClick={handleChangePw} disabled={saving || !newPw || !newPwRepeat}>
                Passwort ändern
              </button>
            </div>
          </div>
          {errMsg && <div style={{ fontSize: 13, color: 'var(--danger)', background: 'var(--danger-bg)', padding: 'var(--space-2) var(--space-3)', borderRadius: 'var(--radius-sm)' }}>{errMsg}</div>}
          {msg && <div style={{ fontSize: 13, color: 'var(--success)', background: 'var(--success-bg)', padding: 'var(--space-2) var(--space-3)', borderRadius: 'var(--radius-sm)' }}>{msg}</div>}

          {/* Konto löschen */}
          <div style={{ borderTop: '1px solid var(--border-light)', paddingTop: 'var(--space-4)', marginTop: 'var(--space-2)' }}>
            <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--danger)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Gefahrenzone</div>
            {!deleteConfirm ? (
              <button
                className="btn btn-sm"
                onClick={() => setDeleteConfirm(true)}
                style={{ color: 'var(--danger)', border: '1px solid var(--danger)', background: 'none', fontSize: 12 }}
              >
                Konto löschen
              </button>
            ) : (
              <div style={{ background: 'var(--danger-bg)', padding: 'var(--space-3)', borderRadius: 'var(--radius-sm)' }}>
                <div style={{ fontSize: 13, color: 'var(--danger)', marginBottom: 8 }}>
                  Dein Konto wird unwiderruflich gelöscht. Aufträge und Dokumente bleiben in der Organisation erhalten.
                </div>
                <div style={{ display: 'flex', gap: 8 }}>
                  <button className="btn btn-sm btn-ghost" onClick={() => setDeleteConfirm(false)}>Abbrechen</button>
                  <button
                    className="btn btn-sm"
                    style={{ background: 'var(--danger)', color: 'white', border: 'none' }}
                    disabled={deleting}
                    onClick={async () => {
                      setDeleting(true);
                      try {
                        const res = await fetch(`${WORKER_URL}/api/account`, {
                          method: 'DELETE',
                          headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
                          body: JSON.stringify({ confirm: 'DELETE' }),
                        });
                        if (res.ok) {
                          const sb = await initSupabase();
                          await sb.auth.signOut();
                          window.location.reload();
                        } else {
                          const d = await res.json();
                          setErrMsg(d.error || 'Fehler beim Löschen');
                        }
                      } catch (e) { setErrMsg(e.message); }
                      setDeleting(false);
                    }}
                  >
                    {deleting ? 'Lösche…' : 'Endgültig löschen'}
                  </button>
                </div>
              </div>
            )}
          </div>

          <div style={{ borderTop: '1px solid var(--border-light)', paddingTop: 'var(--space-4)', marginTop: 'var(--space-2)' }}>
            <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-tertiary)', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Konto</div>
            <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 8, lineHeight: 1.5 }}>
              Um dein Konto und alle zugehörigen Daten löschen zu lassen, sende eine E-Mail an:
            </div>
            <a href="mailto:tobias@thetacklecompany.de?subject=Augenschein%20Konto%20l%C3%B6schen&body=Bitte%20l%C3%B6scht%20mein%20Konto%20und%20alle%20zugehörigen%20Daten.%0A%0AE-Mail%3A%20" style={{
              fontSize: 13, color: 'var(--vl-blue-light, #1E5A8E)', fontWeight: 500,
            }}>
              tobias@thetacklecompany.de
            </a>
            <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>
              Wir löschen dein Konto innerhalb von 7 Werktagen und bestätigen per E-Mail.
            </div>
          </div>
        </div>
        <div className="modal-footer">
          <button className="btn btn-ghost" onClick={onClose}>Schließen</button>
        </div>
      </div>
    </div>
  );
};

const TopBar = ({ view, projekt, aktiverGutachtenIdx, gutachtenCount, onNavigate, onToggleNav }) => {
  return (
    <div className="top-bar">
      <button className="top-bar-toggle" onClick={onToggleNav} title="Navigation ein-/ausklappen" aria-label="Navigation umschalten">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
      </button>
      <div className="breadcrumb">
        <Breadcrumb view={view} projekt={projekt} aktiverGutachtenIdx={aktiverGutachtenIdx} gutachtenCount={gutachtenCount} onNavigate={onNavigate} />
      </div>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════
// 5 · LOGIN + PASSWORT-RESET
// Drei Modi: 'login' | 'request-reset' | 'do-reset'
//   login          Standard E-Mail + Passwort
//   request-reset  User gibt E-Mail ein, bekommt Recovery-Mail
//   do-reset       Kommt aus dem Mail-Link (Supabase triggert
//                  PASSWORD_RECOVERY-Event), hier neues Passwort setzen
// Wird gezeigt, solange keine Vollzugriff-Session aktiv ist.
// ══════════════════════════════════════════════════════════════════

// Redirect-URL für Recovery-Mails. Landet mit #access_token=...&type=recovery
// auf der Startseite, Supabase-JS liest das Fragment automatisch und feuert
// das PASSWORD_RECOVERY-Event, auf das die App-Komponente reagiert.
const PASSWORD_RECOVERY_REDIRECT =
  typeof window !== 'undefined' && window.location
    ? `${window.location.origin}/`
    : 'https://custom.augenschein.app/';

const LoginView = ({ onLoginSuccess, initialMode = 'login' }) => {
  const [mode, setMode] = useState(initialMode);  // 'login' | 'request-reset' | 'do-reset' | 'reset-sent'
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [passwordRepeat, setPasswordRepeat] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [info, setInfo] = useState(null);

  // Wenn die übergeordnete App einen Recovery-Flow ankündigt (via initialMode),
  // sofort in den Reset-Modus wechseln. Das greift, wenn der User aus einer
  // Recovery-Mail zurückkommt.
  useEffect(() => {
    if (initialMode && initialMode !== mode) setMode(initialMode);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [initialMode]);

  const switchMode = (next) => {
    setMode(next);
    setError(null);
    setInfo(null);
    setPassword('');
    setPasswordRepeat('');
  };

  const handleLogin = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    try {
      const sb = await initSupabase();
      const { error: authError } = await sb.auth.signInWithPassword({ email, password });
      if (authError) {
        setError(authError.message);
        setLoading(false);
        return;
      }
      onLoginSuccess();
    } catch (err) {
      setError(err.message || String(err));
      setLoading(false);
    }
  };

  const handleRequestReset = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    try {
      const sb = await initSupabase();
      const { error: resetError } = await sb.auth.resetPasswordForEmail(email, {
        redirectTo: PASSWORD_RECOVERY_REDIRECT,
      });
      if (resetError) {
        setError(resetError.message);
        setLoading(false);
        return;
      }
      setMode('reset-sent');
      setLoading(false);
    } catch (err) {
      setError(err.message || String(err));
      setLoading(false);
    }
  };

  const handleDoReset = async (e) => {
    e.preventDefault();
    if (password.length < 8) {
      setError('Passwort muss mindestens 8 Zeichen haben.');
      return;
    }
    if (password !== passwordRepeat) {
      setError('Die beiden Passwörter stimmen nicht überein.');
      return;
    }
    setLoading(true);
    setError(null);
    try {
      const sb = await initSupabase();
      const { error: updateError } = await sb.auth.updateUser({ password });
      if (updateError) {
        setError(updateError.message);
        setLoading(false);
        return;
      }
      // Supabase hat die Session bereits mit vollen Rechten reaktiviert.
      // Der onAuthStateChange-Listener in der App fängt das Event-Update ab
      // und wechselt automatisch in die Projektliste.
      setInfo('Passwort geändert. Du wirst angemeldet …');
      // Kleine Verzögerung für die visuelle Rückmeldung
      setTimeout(() => onLoginSuccess(), 600);
    } catch (err) {
      setError(err.message || String(err));
      setLoading(false);
    }
  };

  // ─── Formular-Rendering je Modus ───
  let title, subtitle, formContent, footerLinks;

  if (mode === 'do-reset') {
    title = 'Neues Passwort setzen';
    subtitle = 'Wähle ein Passwort, mit dem du dich künftig anmeldest.';
    formContent = (
      <form onSubmit={handleDoReset}>
        <div className="form-field">
          <label className="form-label">Neues Passwort</label>
          <input
            className="form-input"
            type="password"
            value={password}
            onChange={e => setPassword(e.target.value)}
            required
            autoFocus
            minLength={8}
          />
          <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>
            Mindestens 8 Zeichen.
          </div>
        </div>
        <div className="form-field">
          <label className="form-label">Passwort wiederholen</label>
          <input
            className="form-input"
            type="password"
            value={passwordRepeat}
            onChange={e => setPasswordRepeat(e.target.value)}
            required
            minLength={8}
          />
        </div>

        {error && (
          <div style={{
            background: 'var(--danger-bg)', color: 'var(--danger)',
            padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
            fontSize: 13, marginBottom: 'var(--space-4)',
          }}>
            {error}
          </div>
        )}
        {info && (
          <div style={{
            background: 'var(--success-bg)', color: 'var(--success)',
            padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
            fontSize: 13, marginBottom: 'var(--space-4)',
          }}>
            {info}
          </div>
        )}

        <button
          type="submit"
          className="btn btn-primary"
          disabled={loading || !password || !passwordRepeat}
          style={{ width: '100%', padding: '10px 14px' }}
        >
          {loading ? 'Speichere …' : 'Passwort speichern'}
        </button>
      </form>
    );
    footerLinks = null;
  } else if (mode === 'reset-sent') {
    title = 'E-Mail ist unterwegs';
    subtitle = `Wir haben einen Link zum Zurücksetzen an ${email || 'deine E-Mail-Adresse'} geschickt. Öffne die Mail und klicke auf den Link. Du kannst dieses Fenster geöffnet lassen — nach dem Klick geht es direkt hier weiter.`;
    formContent = (
      <button
        type="button"
        className="btn btn-secondary"
        onClick={() => switchMode('login')}
        style={{ width: '100%', padding: '10px 14px' }}
      >
        Zurück zur Anmeldung
      </button>
    );
    footerLinks = null;
  } else if (mode === 'request-reset') {
    title = 'Passwort zurücksetzen';
    subtitle = 'Gib deine E-Mail-Adresse ein. Wir schicken dir einen Link, mit dem du ein neues Passwort setzen kannst.';
    formContent = (
      <form onSubmit={handleRequestReset}>
        <div className="form-field">
          <label className="form-label">E-Mail</label>
          <input
            className="form-input"
            type="email"
            value={email}
            onChange={e => setEmail(e.target.value)}
            required
            autoFocus
          />
        </div>

        {error && (
          <div style={{
            background: 'var(--danger-bg)', color: 'var(--danger)',
            padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
            fontSize: 13, marginBottom: 'var(--space-4)',
          }}>
            {error}
          </div>
        )}

        <button
          type="submit"
          className="btn btn-primary"
          disabled={loading || !email}
          style={{ width: '100%', padding: '10px 14px' }}
        >
          {loading ? 'Sende Link …' : 'Link anfordern'}
        </button>
      </form>
    );
    footerLinks = (
      <button
        type="button"
        onClick={() => switchMode('login')}
        style={{
          background: 'none', border: 'none', padding: 0,
          color: 'var(--vl-blue-light, #1E5A8E)', cursor: 'pointer',
          fontSize: 13, marginTop: 'var(--space-4)',
        }}
      >
        Zurück zur Anmeldung
      </button>
    );
  } else {
    // Default: login
    title = null;
    subtitle = null;
    formContent = (
      <form onSubmit={handleLogin}>
        <div className="form-field">
          <label className="form-label">E-Mail</label>
          <input
            className="form-input"
            type="email"
            value={email}
            onChange={e => setEmail(e.target.value)}
            required
            autoFocus
          />
        </div>
        <div className="form-field">
          <label className="form-label">Passwort</label>
          <input
            className="form-input"
            type="password"
            value={password}
            onChange={e => setPassword(e.target.value)}
            required
          />
        </div>

        {error && (
          <div style={{
            background: 'var(--danger-bg)', color: 'var(--danger)',
            padding: 'var(--space-3)', borderRadius: 'var(--radius-md)',
            fontSize: 13, marginBottom: 'var(--space-4)',
          }}>
            {error}
          </div>
        )}

        <button
          type="submit"
          className="btn btn-primary"
          disabled={loading || !email || !password}
          style={{ width: '100%', padding: '10px 14px' }}
        >
          {loading ? 'Anmelden…' : 'Anmelden'}
        </button>
      </form>
    );
    footerLinks = (
      <button
        type="button"
        onClick={() => switchMode('request-reset')}
        style={{
          background: 'none', border: 'none', padding: 0,
          color: 'var(--vl-blue-light, #1E5A8E)', cursor: 'pointer',
          fontSize: 13, marginTop: 'var(--space-4)',
        }}
      >
        Passwort vergessen?
      </button>
    );
  }

  return (
    <div className="view-wrapper" style={{ maxWidth: 420, margin: '80px auto' }}>
      <div className="card">
        <div style={{ textAlign: 'center', marginBottom: 'var(--space-6)' }}>
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
            <svg width="36" height="36" viewBox="0 0 512 512">
              <g transform="translate(256,256)">
                <rect x="-110" y="-80" width="220" height="260" rx="22" fill="none" stroke="#0A2540" strokeWidth="18"/>
                <rect x="-46" y="-112" width="92" height="58" rx="16" fill="#0A2540"/>
                <rect x="-30" y="-96" width="60" height="28" rx="8" fill="#fff"/>
                <g transform="translate(0, 120)">
                  <path d="M0,-52 C-30,-52 -48,-30 -48,0 C-48,30 0,72 0,72 C0,72 48,30 48,0 C48,-30 30,-52 0,-52Z" fill="#EAAA3C"/>
                  <circle cx="0" cy="-4" r="14" fill="#fff"/>
                </g>
                <line x1="-62" y1="-18" x2="62" y2="-18" stroke="#0A2540" strokeWidth="14" strokeLinecap="round" opacity="0.15"/>
                <line x1="-62" y1="18" x2="36" y2="18" stroke="#0A2540" strokeWidth="14" strokeLinecap="round" opacity="0.15"/>
                <line x1="-62" y1="54" x2="16" y2="54" stroke="#0A2540" strokeWidth="14" strokeLinecap="round" opacity="0.15"/>
              </g>
            </svg>
            <span style={{ fontSize: 24, fontWeight: 700, letterSpacing: '-0.03em', color: '#EAAA3C' }}>Augenschein</span>
          </div>
        </div>

        {title && (
          <div style={{ marginBottom: 'var(--space-5)' }}>
            <div style={{ fontSize: 18, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 6 }}>
              {title}
            </div>
            {subtitle && (
              <div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
                {subtitle}
              </div>
            )}
          </div>
        )}

        {formContent}

        {footerLinks && (
          <div style={{ textAlign: 'center' }}>
            {footerLinks}
          </div>
        )}
      </div>
    </div>
  );
};

// ══════════════════════════════════════════════════════════════════
// NEUER-AUFTRAG-MODAL
// Zwei Wege: manuell anlegen oder aus Dokumenten (KI-Extraktion).
// KI-Tab akzeptiert bis zu zwei Dokumente gleichzeitig: Beschluss
// und Anschreiben. Beide werden nach Projekt-Anlage sequenziell
// extrahiert (siehe App.handleProjektCreated).
// ══════════════════════════════════════════════════════════════════

// ── Kleine Helfer für den KI-Tab: Drop-Zone-Button und Tile bei ausgewählter Datei

const FileDropzoneButton = ({ onClick, title, subtitle }) => (
  <button
    type="button"
    onClick={onClick}
    style={{
      width: '100%',
      padding: 'var(--space-5) var(--space-3)',
      border: '2px dashed var(--border-light)',
      borderRadius: 'var(--radius-md)',
      background: 'var(--surface-light)',
      cursor: 'pointer',
      display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
      color: 'var(--text-secondary)',
      minHeight: 140,
      justifyContent: 'center',
    }}
  >
    <IconUpload size={28} stroke="var(--text-tertiary)" />
    <div style={{ fontSize: 13, fontWeight: 600, textAlign: 'center' }}>{title}</div>
    <div style={{ fontSize: 11, color: 'var(--text-tertiary)', textAlign: 'center', lineHeight: 1.4 }}>
      {subtitle}
    </div>
  </button>
);

const FilePickedTile = ({ file, onRemove }) => (
  <div style={{
    padding: 'var(--space-3)',
    border: '1px solid var(--success)',
    background: 'var(--success-bg)',
    borderRadius: 'var(--radius-md)',
    display: 'flex', alignItems: 'center', gap: 'var(--space-3)',
    minHeight: 140,
  }}>
    <IconDocument size={24} stroke="var(--success)" />
    <div style={{ flex: 1, minWidth: 0 }}>
      <div style={{
        fontWeight: 600, fontSize: 13,
        overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
      }}>
        {file.name}
      </div>
      <div style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
        {(file.size / 1024).toFixed(0)} KB
      </div>
    </div>
    <button
      type="button"
      onClick={onRemove}
      style={{
        background: 'none', border: 'none', color: 'var(--danger)',
        cursor: 'pointer', fontSize: 12,
      }}
    >
      Entfernen
    </button>
  </div>
);

// ────────────────────────────────────────────────────────────────────
// AuftragReviewModal — Lädt + extrahiert mehrere Dokumente parallel,
// führt die Ergebnisse zusammen und zeigt EIN Review-Modal.
// ────────────────────────────────────────────────────────────────────
async function extractDocumentSSE(file, typ, projektId, gutachtenId, session, workerUrl, onProgress) {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('project_id', projektId);
  formData.append('scope', 'auftrag');
  formData.append('typ', typ);
  if (gutachtenId) formData.append('gutachten_id', gutachtenId);

  const res = await fetch(`${workerUrl}/api/extract/document`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${session.access_token}` },
    body: formData,
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let result = null;

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let idx;
    while ((idx = buffer.indexOf('\n\n')) !== -1) {
      const block = buffer.slice(0, idx);
      buffer = buffer.slice(idx + 2);
      const evtM = block.match(/^event:\s*(\S+)/m);
      const datM = block.match(/^data:\s*(.*)$/m);
      if (!evtM || !datM) continue;
      let data;
      try { data = JSON.parse(datM[1]); } catch { continue; }
      if (evtM[1] === 'progress' && onProgress) onProgress(data);
      else if (evtM[1] === 'result') result = data;
      else if (evtM[1] === 'error') throw new Error(data.message || 'Extraktionsfehler');
    }
  }
  return result;
}

function mergeExtractions(results) {
  // results = [{ typ, dokumentId, fields }, ...] — Beschluss zuerst
  const merged = {};
  const beteiligte = [];
  const seenNames = new Set();

  for (const r of results) {
    if (!r?.fields) continue;
    for (const [key, val] of Object.entries(r.fields)) {
      if (key === 'beteiligte') continue;
      if (key === 'titel_vorschlag') continue;
      if (key === 'eigentuemer') continue;
      if (key === 'versteigerungsobjekte') {
        if (!merged[key]) merged[key] = val;
        continue;
      }
      // Erstes Dokument (Beschluss) hat Priorität, zweites füllt nur Lücken
      if (val != null && !merged[key]) {
        merged[key] = { value: val, source: r.typ };
      }
    }
    // Beteiligte zusammenführen, fuzzy-Dedup per Nachname (umlaut-normalisiert)
    const bet = Array.isArray(r.fields.beteiligte) ? r.fields.beteiligte : [];
    for (const b of bet) {
      const normName = (b.name || '').toLowerCase()
        .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
        .replace(/[,.\s]+/g, ' ').trim();
      const nachname = normName.split(' ')[0];
      const normRolle = (b.rolle || '').toLowerCase()
        .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss');
      const dedup = `${normRolle}:${nachname}`;
      if (seenNames.has(dedup)) continue;
      seenNames.add(dedup);
      beteiligte.push({ ...b, _source: r.typ });
    }
  }
  return { merged, beteiligte };
}

const AuftragReviewModal = ({ files, projektId, gutachtenId, session, workerUrl, projektName, onRenameProjekt, onClose, onComplete }) => {
  const showToast = useToast();
  const [phase, setPhase] = useState('extracting'); // 'extracting' | 'review' | 'applying'
  const [progress, setProgress] = useState({}); // { [idx]: { step, pct } }
  const [error, setError] = useState(null);
  const [results, setResults] = useState([]); // [{ typ, dokumentId, fields }]
  const [mergedData, setMergedData] = useState(null);
  const [editedFields, setEditedFields] = useState({}); // { key: editedValue }
  const [editedBeteiligte, setEditedBeteiligte] = useState([]);
  const [editedName, setEditedName] = useState('');
  const [applying, setApplying] = useState(false);

  // Hooks für PDF-Viewer (müssen IMMER aufgerufen werden, nicht konditional)
  const [viewerFileIdx, setViewerFileIdx] = useState(0);
  const fileUrls = useMemo(() => files.map(f => URL.createObjectURL(f.file)), [files]);
  useEffect(() => () => fileUrls.forEach(u => URL.revokeObjectURL(u)), [fileUrls]);

  const isQuickCreate = projektName && projektName.startsWith('Neuer Auftrag vom');

  // Alle Dokumente parallel extrahieren
  useEffect(() => {
    let active = true;
    (async () => {
      try {
        const promises = files.map((f, i) =>
          extractDocumentSSE(f.file, f.typ, projektId, gutachtenId, session, workerUrl,
            (p) => { if (active) setProgress(prev => ({ ...prev, [i]: p })); }
          ).then(result => ({
            typ: f.typ,
            dokumentId: result?.dokument_id,
            fields: result?.extracted_fields?.fields || {},
          }))
        );
        const allResults = await Promise.all(promises);
        if (!active) return;
        setResults(allResults);

        // Zusammenführen
        const { merged, beteiligte } = mergeExtractions(allResults);
        setMergedData(merged);
        setEditedBeteiligte(beteiligte);

        setPhase('review');
      } catch (err) {
        if (active) setError(err.message || String(err));
      }
    })();
    return () => { active = false; };
  }, []);

  const handleApply = async () => {
    setApplying(true);
    try {
      // Für jedes Dokument apply-extraction aufrufen
      for (const r of results) {
        if (!r.dokumentId) continue;
        const scalars = Object.keys(r.fields).filter(k =>
          !['beteiligte', 'eigentuemer', 'titel_vorschlag', 'versteigerungsobjekte', 'notizen', 'kontakt_anschrift'].includes(k)
          && r.fields[k] != null
        );
        // Editierte Werte als custom_values
        const cv = {};
        for (const key of scalars) {
          if (editedFields[key] !== undefined) cv[key] = editedFields[key];
        }
        const betArr = Array.isArray(r.fields.beteiligte) ? r.fields.beteiligte : [];
        const applyRes = await fetch(`${workerUrl}/api/apply-extraction`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` },
          body: JSON.stringify({
            dokument_id: r.dokumentId,
            applied_fields: scalars,
            custom_values: cv,
            applied_beteiligte_idx: betArr.map((_, i) => i),
            force_overwrite: [],
            document_title: null,
          }),
        });
        if (!applyRes.ok) {
          const errBody = await applyRes.json().catch(() => ({}));
          console.error('[AuftragReview] apply-extraction failed:', applyRes.status, errBody);
          throw new Error(errBody.error || `Übernahme fehlgeschlagen (HTTP ${applyRes.status})`);
        }
      }
      showToast(`Daten aus ${results.length} Dokument${results.length > 1 ? 'en' : ''} übernommen`, 'success');
      if (onComplete) await onComplete();
      onClose();
    } catch (err) {
      setError(err.message || String(err));
      setApplying(false);
    }
  };

  const typLabel = (t) => ({ gerichtsbeschluss: 'Beweisbeschluss', anschreiben: 'Auftrag' }[t] || t);
  const sourceColor = (t) => t === 'gerichtsbeschluss' ? 'var(--vl-blue)' : 'var(--vl-orange, #F59E0B)';

  // ── Extraktions-Fortschritt ──
  if (phase === 'extracting') {
    return (
      <div className="modal-backdrop">
        <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 480 }}>
          <div className="modal-header">
            <div className="modal-title">Dokumente werden ausgelesen</div>
            <button className="modal-close" onClick={onClose}>×</button>
          </div>
          <div className="modal-body" style={{ padding: 'var(--space-5)' }}>
            {files.map((f, i) => {
              const p = progress[i];
              const pct = p?.pct || 0;
              const stepLabel = !p ? 'Warte...' : p.step === 'uploading' ? 'Hochladen...' : p.step === 'extracting_text' ? 'Text extrahieren...' : 'KI liest aus...';
              return (
                <div key={i} style={{ marginBottom: 'var(--space-4)' }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4, fontSize: 13 }}>
                    <span style={{ fontWeight: 600 }}>{typLabel(f.typ)}</span>
                    <span style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>{stepLabel}</span>
                  </div>
                  <div style={{ height: 6, borderRadius: 3, background: 'var(--border-light)', overflow: 'hidden' }}>
                    <div style={{ height: '100%', width: `${pct}%`, borderRadius: 3, background: 'var(--vl-blue)', transition: 'width 0.3s' }} />
                  </div>
                </div>
              );
            })}
            {error && (
              <div style={{ padding: 'var(--space-3)', background: 'var(--danger-bg)', color: 'var(--danger)', borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
                {error}
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }

  // ── Review: Zusammengeführte Daten ──
  if (phase === 'review' && mergedData) {
    const fieldKeys = Object.keys(mergedData).filter(k => k !== 'versteigerungsobjekte');
    const isMobile = window.innerWidth < 768;

    return (
      <div className="modal-backdrop">
        <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 1200, width: '95vw', display: 'flex', flexDirection: 'column', maxHeight: '92vh' }}>
          <div className="modal-header">
            <div>
              <div className="modal-title">Auftragsdaten prüfen</div>
              <div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 }}>
                Zusammengeführt aus {results.length} Dokument{results.length > 1 ? 'en' : ''}. Klicke auf einen Wert zum Bearbeiten.
              </div>
            </div>
            <button className="modal-close" onClick={onClose}>×</button>
          </div>

          <div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', flex: 1, overflow: 'hidden' }}>
            {/* Dokument-Viewer: auf Mobile kollabierbar */}
            {isMobile ? (
              <details style={{ borderBottom: '1px solid var(--border-light)', flexShrink: 0 }}>
                <summary style={{
                  padding: '10px 16px', cursor: 'pointer', fontSize: 13, fontWeight: 600,
                  color: 'var(--text-secondary)', background: 'var(--surface-light)',
                  userSelect: 'none',
                }}>
                  Dokument anzeigen
                </summary>
                <div style={{ height: '40vh' }}>
                  {files.length > 1 && (
                    <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--border-light)', background: 'var(--surface-light)', padding: '0 8px' }}>
                      {files.map((f, i) => (
                        <button key={i} onClick={() => setViewerFileIdx(i)} style={{
                          padding: '6px 12px', fontSize: 11, fontWeight: viewerFileIdx === i ? 600 : 400,
                          color: viewerFileIdx === i ? 'var(--vl-blue)' : 'var(--text-tertiary)',
                          background: 'none', border: 'none', cursor: 'pointer',
                          borderBottom: viewerFileIdx === i ? '2px solid var(--vl-blue)' : '2px solid transparent',
                        }}>
                          {f.file.name.length > 20 ? f.file.name.slice(0, 17) + '…' : f.file.name}
                        </button>
                      ))}
                    </div>
                  )}
                  <iframe
                    src={fileUrls[viewerFileIdx]}
                    style={{ width: '100%', height: '100%', border: 'none', background: '#f5f5f5' }}
                    title="Dokument-Vorschau"
                  />
                </div>
              </details>
            ) : (
              <div style={{ flex: '0 0 50%', display: 'flex', flexDirection: 'column', borderRight: '1px solid var(--border-light)' }}>
                {files.length > 1 && (
                  <div style={{
                    display: 'flex', gap: 0, borderBottom: '1px solid var(--border-light)',
                    background: 'var(--surface-light)', padding: '0 8px', flexShrink: 0,
                  }}>
                    {files.map((f, i) => (
                      <button key={i} onClick={() => setViewerFileIdx(i)} style={{
                        padding: '8px 14px', fontSize: 12, fontWeight: viewerFileIdx === i ? 600 : 400,
                        color: viewerFileIdx === i ? 'var(--vl-blue)' : 'var(--text-tertiary)',
                        background: 'none', border: 'none', cursor: 'pointer',
                        borderBottom: viewerFileIdx === i ? '2px solid var(--vl-blue)' : '2px solid transparent',
                        marginBottom: -1,
                      }}>
                        {f.file.name.length > 25 ? f.file.name.slice(0, 22) + '…' : f.file.name}
                      </button>
                    ))}
                  </div>
                )}
                <iframe
                  src={fileUrls[viewerFileIdx]}
                  style={{ flex: 1, width: '100%', border: 'none', background: '#f5f5f5' }}
                  title="Dokument-Vorschau"
                />
              </div>
            )}

            {/* Rechte Seite / Unten: Extrahierte Daten */}
            <div className="modal-body" style={{ padding: 0, overflow: 'auto', flex: 1 }}>

            {/* Felder */}
            <div style={{ padding: 'var(--space-4) var(--space-5)' }}>
              <div style={{ fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-tertiary)', marginBottom: 'var(--space-3)' }}>
                Felder ({fieldKeys.length})
              </div>
              {fieldKeys.map(key => {
                const { value, source } = mergedData[key];
                const isEdited = editedFields[key] !== undefined;
                const displayValue = isEdited ? editedFields[key] : value;
                return (
                  <div key={key} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 0', borderBottom: '1px solid var(--border-light)' }}>
                    <div style={{ flex: '0 0 140px', fontSize: 12, color: 'var(--text-secondary)' }}>
                      {FELD_LABELS[key] || key}
                    </div>
                    <div style={{ flex: 1, fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}
                      contentEditable suppressContentEditableWarning
                      onBlur={e => {
                        const newVal = e.currentTarget.textContent.trim();
                        if (newVal !== String(value)) setEditedFields(prev => ({ ...prev, [key]: newVal }));
                      }}
                    >
                      {formatFieldValue(key, displayValue)}
                    </div>
                    <span style={{ fontSize: 10, fontWeight: 600, color: sourceColor(source), flexShrink: 0 }}>
                      {typLabel(source)}
                    </span>
                  </div>
                );
              })}
            </div>

            {/* Beteiligte */}
            {editedBeteiligte.length > 0 && (
              <div style={{ padding: 'var(--space-4) var(--space-5)', borderTop: '1px solid var(--border-light)' }}>
                <div style={{ fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-tertiary)', marginBottom: 'var(--space-3)' }}>
                  Beteiligte ({editedBeteiligte.length})
                </div>
                {editedBeteiligte.map((b, i) => (
                  <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 0', borderBottom: '1px solid var(--border-light)' }}>
                    <span style={{ fontSize: 12, color: 'var(--vl-blue)', fontWeight: 600, minWidth: 100 }}>{b.rolle}</span>
                    <span style={{ flex: 1, fontSize: 13, fontWeight: 500 }}>{b.name}</span>
                    {b.anschrift && <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{b.anschrift}</span>}
                    <span style={{ fontSize: 10, fontWeight: 600, color: sourceColor(b._source), flexShrink: 0 }}>
                      {typLabel(b._source)}
                    </span>
                  </div>
                ))}
              </div>
            )}

            {error && (
              <div style={{ margin: 'var(--space-4) var(--space-5)', padding: 'var(--space-3)', background: 'var(--danger-bg)', color: 'var(--danger)', borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
                {error}
              </div>
            )}
          </div>
          </div>{/* Ende Split-Layout */}

          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-3)', padding: 'var(--space-4) var(--space-5)', borderTop: '1px solid var(--border-light)', background: 'var(--surface-light)' }}>
            <button className="btn btn-ghost" onClick={onClose} disabled={applying}>Abbrechen</button>
            <button className="btn btn-primary" onClick={handleApply} disabled={applying}>
              {applying ? 'Wird übernommen...' : `${fieldKeys.length} Felder + ${editedBeteiligte.length} Beteiligte übernehmen`}
            </button>
          </div>
        </div>
      </div>
    );
  }

  return null;
};

// ════════════════════════════════════════════════════════════════════
// BULK UPLOAD MODAL — Mehrere Dokumente auf einmal hochladen,
// automatisch klassifizieren, reviewen und verarbeiten.
// ════════════════════════════════════════════════════════════════════

// Upload + Extraktion mit dynamischem scope (für Bulk)
async function bulkExtractDoc(file, typ, scope, projektId, gutachtenId, objektId, session, workerUrl, onProgress) {
  // Frischen Token holen
  let token = session.access_token;
  try {
    const sb = await initSupabase();
    const { data } = await sb.auth.getSession();
    if (data?.session?.access_token) token = data.session.access_token;
  } catch {}

  const fd = new FormData();
  fd.append('file', file);
  fd.append('project_id', projektId);
  fd.append('scope', scope || 'auftrag');
  fd.append('typ', typ);
  if (gutachtenId) fd.append('gutachten_id', gutachtenId);
  if (objektId) fd.append('objekt_id', objektId);

  const res = await fetch(`${workerUrl}/api/extract/document`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}` },
    body: fd,
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${res.status}`);
  }

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let extractResult = null;
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let idx;
    while ((idx = buffer.indexOf('\n\n')) !== -1) {
      const block = buffer.slice(0, idx);
      buffer = buffer.slice(idx + 2);
      const evtM = block.match(/^event:\s*(\S+)/m);
      const datM = block.match(/^data:\s*(.*)$/m);
      if (!evtM || !datM) continue;
      let data;
      try { data = JSON.parse(datM[1]); } catch { continue; }
      if (evtM[1] === 'progress' && onProgress) onProgress(data);
      else if (evtM[1] === 'result') extractResult = data;
      else if (evtM[1] === 'error') throw new Error(data.message || 'Extraktionsfehler');
    }
  }

  // Extraktion abgeschlossen — Felder zurückgeben (Apply passiert im Review-Schritt)
  const env = extractResult?.extracted_fields;
  const skip = new Set(['beteiligte','eigentuemer','notizen','kontakt_anschrift','titel_vorschlag','versteigerungsobjekte']);
  const scalarKeys = env?.fields
    ? Object.keys(env.fields).filter(k => {
        if (skip.has(k)) return false;
        const v = env.fields[k];
        if (v == null) return false;
        // Arrays und Objekte sind keine Skalarfelder
        if (Array.isArray(v) || (typeof v === 'object' && v !== null)) return false;
        return true;
      })
    : [];
  // Komplexe Felder (Arrays) separat sammeln für die Anzeige
  const complexKeys = env?.fields
    ? Object.keys(env.fields).filter(k => {
        if (skip.has(k)) return false;
        const v = env.fields[k];
        return Array.isArray(v) && v.length > 0;
      })
    : [];

  return {
    dokumentId: extractResult?.dokument_id || null,
    extractedFields: env?.fields || null,
    scalarKeys,
    complexKeys,
    beteiligteArr: Array.isArray(env?.fields?.beteiligte) ? env.fields.beteiligte : [],
  };
}

const BulkUploadModal = ({ projektId, gutachtenId, allObjekte, session, workerUrl, onClose, onComplete }) => {
  const registryTypes = useUploadTypes();
  const effectiveTypes = registryTypes || UPLOAD_TYPES;
  const allTypes = useMemo(() => {
    const list = [];
    for (const group of effectiveTypes) {
      for (const item of group.items) {
        list.push({ id: item.id, label: item.label, scope: group.scope, group: group.group });
      }
    }
    return list;
  }, [effectiveTypes]);

  // select | classifying | review | uploading | field_review | applying | done
  const [phase, setPhase] = useState('select');
  const [files, setFiles] = useState([]);
  const [classifications, setClassifications] = useState([]);
  const [classifyProgress, setClassifyProgress] = useState({ current: 0, total: 0 });
  const [uploadProgress, setUploadProgress] = useState({});
  const [error, setError] = useState(null);
  const fileInputRef = useRef(null);
  const [dragOver, setDragOver] = useState(false);

  // Field-Review: pro Dokument extrahierte Felder + Checkboxen
  const [extractionResults, setExtractionResults] = useState([]);
  const [openDocIdx, setOpenDocIdx] = useState(null);
  const [applySummary, setApplySummary] = useState(null);
  const [reviewPdfUrl, setReviewPdfUrl] = useState(null); // PDF-URL für Split-View

  const addFiles = (newFiles) => {
    const pdfs = Array.from(newFiles).filter(f =>
      f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
    );
    if (pdfs.length === 0) return;
    setFiles(prev => [...prev, ...pdfs]);
  };
  const removeFile = (idx) => setFiles(prev => prev.filter((_, i) => i !== idx));

  // ── Klassifikation ──
  const startClassification = async () => {
    if (files.length === 0) return;
    setPhase('classifying');
    setError(null);
    setClassifyProgress({ current: 0, total: files.length });
    const fd = new FormData();
    files.forEach((f, i) => fd.append(`file_${i}`, f));
    try {
      let token = session.access_token;
      try { const sb = await initSupabase(); const { data } = await sb.auth.getSession(); if (data?.session?.access_token) token = data.session.access_token; } catch {}
      const res = await fetch(`${workerUrl}/api/classify-documents`, {
        method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: fd,
      });
      if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || `HTTP ${res.status}`); }
      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';
      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });
        let idx;
        while ((idx = buffer.indexOf('\n\n')) !== -1) {
          const block = buffer.slice(0, idx); buffer = buffer.slice(idx + 2);
          const evtM = block.match(/^event:\s*(\S+)/m);
          const datM = block.match(/^data:\s*(.*)$/m);
          if (!evtM || !datM) continue;
          let data; try { data = JSON.parse(datM[1]); } catch { continue; }
          if (evtM[1] === 'progress') setClassifyProgress({ current: data.index + 1, total: data.total });
          else if (evtM[1] === 'result') {
            const defaultObjId = allObjekte.length === 1 ? allObjekte[0].id : null;
            setClassifications(data.classifications.map(c => ({
              ...c, objekt_id: (c.scope === 'objekt' && defaultObjId) ? defaultObjId : null,
            })));
            setPhase('review');
          } else if (evtM[1] === 'error') throw new Error(data.message || 'Klassifikation fehlgeschlagen');
        }
      }
    } catch (err) {
      // Nicht auf 'select' zurücksetzen — Retry-Button wird in der classifying-Phase angezeigt.
      // (phase im Closure ist der alte Wert, aber setPhase('classifying') wurde bereits aufgerufen)
      setError(err.message || String(err));
    }
  };

  const updateClassification = (index, field, value) => {
    setClassifications(prev => prev.map(c => {
      if (c.index !== index) return c;
      const updated = { ...c, [field]: value };
      if (field === 'typ_id') {
        const entry = allTypes.find(t => t.id === value);
        if (entry) { updated.scope = entry.scope; updated.group = entry.group; updated.typ_label = entry.label; }
      }
      return updated;
    }));
  };

  // ── Upload + Extraktion (ohne Apply) ──
  const startProcessing = async () => {
    setPhase('uploading');
    setError(null);
    const progress = {};
    classifications.forEach(c => { progress[c.index] = { step: 'waiting', pct: 0 }; });
    setUploadProgress({ ...progress });

    const results = [];
    for (const c of classifications) {
      const file = files[c.index];
      if (!file) continue;
      try {
        setUploadProgress(prev => ({ ...prev, [c.index]: { step: 'uploading', pct: 10 } }));
        const result = await bulkExtractDoc(
          file, c.typ_id, c.scope, projektId, gutachtenId,
          c.objekt_id || null, session, workerUrl,
          (prog) => setUploadProgress(prev => ({ ...prev, [c.index]: { step: prog.step || 'extracting', pct: prog.pct || 50 } }))
        );
        setUploadProgress(prev => ({ ...prev, [c.index]: { step: 'done', pct: 100 } }));
        results.push({
          index: c.index, filename: c.filename,
          typ_id: c.typ_id, typ_label: c.typ_label,
          dokumentId: result.dokumentId,
          fields: result.extractedFields,
          scalarKeys: result.scalarKeys,
          complexKeys: result.complexKeys || [],
          beteiligteArr: result.beteiligteArr,
          accepted: new Set(result.scalarKeys),
          acceptedBet: new Set(result.beteiligteArr.map((_, i) => i)),
        });
      } catch (err) {
        setUploadProgress(prev => ({ ...prev, [c.index]: { step: 'error', pct: 0, error: err.message } }));
        results.push({ index: c.index, filename: c.filename, typ_id: c.typ_id, typ_label: c.typ_label, error: err.message });
      }
    }
    setExtractionResults(results);
    const hasFields = results.some(r => r.scalarKeys?.length > 0 || r.beteiligteArr?.length > 0);
    if (hasFields) {
      const firstIdx = results.findIndex(r => (r.scalarKeys?.length > 0) || (r.beteiligteArr?.length > 0));
      setOpenDocIdx(firstIdx);
      setPhase('field_review');
      // PDF für erstes Dokument automatisch laden
      const firstDoc = results[firstIdx];
      if (firstDoc?.dokumentId) {
        (async () => {
          try {
            let token = session.access_token;
            try { const sb = await initSupabase(); const { data } = await sb.auth.getSession(); if (data?.session?.access_token) token = data.session.access_token; } catch {}
            const res = await fetch(
              `${workerUrl}/api/document-url?id=${encodeURIComponent(firstDoc.dokumentId)}`,
              { headers: { Authorization: `Bearer ${token}` } }
            );
            if (res.ok) { const { url } = await res.json(); setReviewPdfUrl(url); }
          } catch {}
        })();
      }
    } else {
      setApplySummary({ applied: 0, total: results.length });
      setPhase('done');
      if (onComplete) onComplete();
    }
  };

  // ── Retry einzelner fehlgeschlagener Extraktion ──
  const [retryingIdx, setRetryingIdx] = useState(null);
  const retryExtraction = async (rIdx) => {
    const r = extractionResults[rIdx];
    if (!r || !r.error) return;
    const c = classifications.find(cl => cl.index === r.index);
    const file = files[r.index];
    if (!c || !file) return;

    setRetryingIdx(rIdx);
    try {
      const result = await bulkExtractDoc(
        file, c.typ_id, c.scope, projektId, gutachtenId,
        c.objekt_id || null, session, workerUrl,
        () => {}
      );
      // Erfolg: fehlerhafte Zeile durch Ergebnis ersetzen
      setExtractionResults(prev => prev.map((item, i) => {
        if (i !== rIdx) return item;
        return {
          index: r.index, filename: r.filename,
          typ_id: r.typ_id, typ_label: r.typ_label,
          dokumentId: result.dokumentId,
          fields: result.extractedFields,
          scalarKeys: result.scalarKeys,
          complexKeys: result.complexKeys || [],
          beteiligteArr: result.beteiligteArr,
          accepted: new Set(result.scalarKeys),
          acceptedBet: new Set(result.beteiligteArr.map((_, j) => j)),
          error: undefined,
        };
      }));
    } catch (err) {
      // Retry auch fehlgeschlagen: Fehlermeldung aktualisieren
      setExtractionResults(prev => prev.map((item, i) =>
        i === rIdx ? { ...item, error: `Retry fehlgeschlagen: ${err.message}` } : item
      ));
    } finally {
      setRetryingIdx(null);
    }
  };

  // ── Feld-Toggles ──
  const toggleField = (resultIdx, fieldKey) => {
    setExtractionResults(prev => prev.map((r, i) => {
      if (i !== resultIdx) return r;
      const next = new Set(r.accepted);
      if (next.has(fieldKey)) next.delete(fieldKey); else next.add(fieldKey);
      return { ...r, accepted: next };
    }));
  };

  // Manuell eingegebener Wert für ein SOLL-Feld, das die KI nicht extrahiert hat
  // (oder wo der User den extrahierten Wert korrigiert). Auto-accept bei Eingabe.
  const setManualValue = (resultIdx, fieldKey, value) => {
    setExtractionResults(prev => prev.map((r, i) => {
      if (i !== resultIdx) return r;
      const manualValues = { ...(r.manualValues || {}) };
      const accepted = new Set(r.accepted);
      const isEmpty = value === '' || value === null || value === undefined;
      if (isEmpty) {
        delete manualValues[fieldKey];
        // Falls die KI auch nichts hatte: aus accepted entfernen
        const hadExtracted = r.fields?.[fieldKey] !== undefined && r.fields?.[fieldKey] !== null && r.fields?.[fieldKey] !== '';
        if (!hadExtracted) accepted.delete(fieldKey);
      } else {
        manualValues[fieldKey] = value;
        accepted.add(fieldKey); // auto-accept
      }
      return { ...r, manualValues, accepted };
    }));
  };

  // Dokument aufklappen und PDF laden
  const openReviewDoc = async (rIdx) => {
    if (openDocIdx === rIdx) { setOpenDocIdx(null); setReviewPdfUrl(null); return; }
    setOpenDocIdx(rIdx);
    setReviewPdfUrl(null);
    const r = extractionResults[rIdx];
    if (r?.dokumentId) {
      try {
        let token = session.access_token;
        try { const sb = await initSupabase(); const { data } = await sb.auth.getSession(); if (data?.session?.access_token) token = data.session.access_token; } catch {}
        console.log('[BulkReview] Loading PDF for dok:', r.dokumentId);
        const res = await fetch(
          `${workerUrl}/api/document-url?id=${encodeURIComponent(r.dokumentId)}`,
          { headers: { Authorization: `Bearer ${token}` } }
        );
        if (res.ok) {
          const { url } = await res.json();
          console.log('[BulkReview] PDF URL loaded:', url?.substring(0, 60));
          setReviewPdfUrl(url);
        } else {
          console.error('[BulkReview] PDF URL failed:', res.status);
        }
      } catch (e) {
        console.error('[BulkReview] PDF URL error:', e.message);
      }
    } else {
      console.warn('[BulkReview] No dokumentId for index:', rIdx);
    }
  };
  const toggleAllFields = (resultIdx) => {
    setExtractionResults(prev => prev.map((r, i) => {
      if (i !== resultIdx) return r;
      // Alle Felder die mind. einen Wert haben (extrahiert ODER manuell) berücksichtigen
      const manualKeys = Object.keys(r.manualValues || {});
      const allKeys = [...new Set([...(r.scalarKeys || []), ...manualKeys])];
      const allChecked = allKeys.length > 0 && allKeys.every(k => r.accepted.has(k));
      return { ...r, accepted: allChecked ? new Set() : new Set(allKeys) };
    }));
  };
  const toggleBet = (resultIdx, betIdx) => {
    setExtractionResults(prev => prev.map((r, i) => {
      if (i !== resultIdx) return r;
      const next = new Set(r.acceptedBet);
      if (next.has(betIdx)) next.delete(betIdx); else next.add(betIdx);
      return { ...r, acceptedBet: next };
    }));
  };

  // ── Apply ──
  const startApply = async () => {
    setPhase('applying');
    let token = session.access_token;
    try { const sb = await initSupabase(); const { data } = await sb.auth.getSession(); if (data?.session?.access_token) token = data.session.access_token; } catch {}

    let totalApplied = 0;
    for (const r of extractionResults) {
      if (!r.dokumentId || r.error) continue;
      const customValues = r.manualValues || {};
      const acceptedFields = Array.from(r.accepted || []);
      // Manuell eingegebene Keys müssen auch als "applied" markiert sein
      const fields = [...new Set([...acceptedFields, ...Object.keys(customValues)])];
      // Manuelle Eingaben sind expliziter User-Wille → sollen DB-Konflikte überschreiben.
      // KI-Werte ohne manuelle Korrektur respektieren bestehende DB-Werte (Konflikt-Handling).
      const forceOverwrite = Object.keys(customValues);
      const betIdx = Array.from(r.acceptedBet || []);
      if (fields.length === 0 && betIdx.length === 0) continue;
      try {
        const applyRes = await fetch(`${workerUrl}/api/apply-extraction`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
          body: JSON.stringify({
            dokument_id: r.dokumentId, applied_fields: fields, custom_values: customValues,
            applied_beteiligte_idx: betIdx, force_overwrite: forceOverwrite, document_title: null,
          }),
        });
        if (applyRes.ok) {
          const result = await applyRes.json();
          totalApplied += result.written_count || 0;
        }
      } catch {}
    }
    setApplySummary({ applied: totalApplied, total: extractionResults.length });
    setPhase('done');
    if (onComplete) onComplete();  // Immer refreshen: Dateien wurden hochgeladen
  };

  const totalAccepted = extractionResults.reduce((s, r) => s + (r.accepted?.size || 0) + (r.acceptedBet?.size || 0), 0);

  return (
    <div className="modal-backdrop"
      onMouseDown={(e) => { e.currentTarget._mouseDownTarget = e.target; }}
      onClick={(e) => {
        // Nur schließen wenn mousedown UND click auf dem Backdrop selbst waren
        // UND wir in einer sicheren Phase sind (nicht mitten in der Verarbeitung)
        if (e.target === e.currentTarget && e.currentTarget._mouseDownTarget === e.currentTarget
            && ['select', 'done'].includes(phase)) onClose();
      }}>
      <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: (phase === 'field_review' && reviewPdfUrl) ? 1200 : phase === 'field_review' ? 860 : 720, maxHeight: '92vh', display: 'flex', flexDirection: 'column' }}>
        <div className="modal-header">
          <div>
            <div className="modal-title">Stapel hochladen</div>
            <div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 2 }}>
              {phase === 'select' && 'PDFs auswählen oder hierher ziehen'}
              {phase === 'classifying' && `Erkenne Dokumenttypen… ${classifyProgress.current}/${classifyProgress.total}`}
              {phase === 'review' && `${classifications.length} Dokumente erkannt — bitte prüfen`}
              {phase === 'uploading' && 'Lade hoch und extrahiere…'}
              {phase === 'field_review' && 'Extrahierte Daten prüfen — Felder an-/abwählen'}
              {phase === 'applying' && 'Übernehme Daten…'}
              {phase === 'done' && (applySummary?.applied > 0 ? `${applySummary.applied} Felder übernommen` : 'Fertig')}
            </div>
          </div>
          {!['uploading', 'applying', 'classifying'].includes(phase) && (
            <button className="modal-close" onClick={onClose}>×</button>
          )}
        </div>

        <div className="modal-body" style={{
          padding: phase === 'field_review' ? 0 : 'var(--space-4)',
          overflowY: phase === 'field_review' && reviewPdfUrl ? 'hidden' : 'auto',
          display: phase === 'field_review' && reviewPdfUrl ? 'flex' : 'block',
          flex: 1, minHeight: 0,
        }}>
          {error && phase !== 'classifying' && (
            <div style={{ padding: 'var(--space-3)', margin: phase === 'field_review' ? 0 : undefined, marginBottom: 'var(--space-4)', background: 'rgba(220,38,38,0.06)', border: '1px solid var(--danger)', borderRadius: 'var(--radius-sm)', fontSize: 13, color: 'var(--danger)' }}>{error}</div>
          )}

          {/* ── Phase: Dateiauswahl ── */}
          {phase === 'select' && (
            <>
              <div
                onDragOver={e => { e.preventDefault(); setDragOver(true); }}
                onDragLeave={() => setDragOver(false)}
                onDrop={e => { e.preventDefault(); setDragOver(false); addFiles(e.dataTransfer.files); }}
                onClick={() => fileInputRef.current?.click()}
                style={{
                  border: `2px dashed ${dragOver ? 'var(--vl-blue)' : 'var(--border-light)'}`,
                  borderRadius: 'var(--radius-lg)', padding: files.length > 0 ? '16px 20px' : '40px 20px',
                  textAlign: 'center', cursor: 'pointer',
                  background: dragOver ? 'var(--surface-blue, #EEF3F8)' : 'transparent',
                  transition: 'all 0.15s', marginBottom: 'var(--space-4)',
                }}
              >
                <input ref={fileInputRef} type="file" accept=".pdf,application/pdf" multiple
                  onChange={e => { addFiles(e.target.files); e.target.value = ''; }} style={{ display: 'none' }} />
                <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 4 }}>PDFs hierher ziehen</div>
                <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>oder klicken zum Auswählen</div>
              </div>
              {files.length > 0 && files.map((f, i) => (
                <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', borderBottom: '1px solid var(--border-light)', fontSize: 13 }}>
                  <IconDocument size={16} />
                  <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</span>
                  <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{(f.size / 1024).toFixed(0)} KB</span>
                  <button onClick={() => removeFile(i)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-tertiary)', fontSize: 16, padding: '0 4px' }}>×</button>
                </div>
              ))}
            </>
          )}

          {/* ── Phase: Klassifikation ── */}
          {phase === 'classifying' && (
            <div style={{ textAlign: 'center', padding: 'var(--space-6)' }}>
              {!error ? (
                <>
                  <div style={{ width: '100%', height: 6, background: 'var(--border-light)', borderRadius: 3, overflow: 'hidden', marginBottom: 'var(--space-4)' }}>
                    <div style={{ height: '100%', background: 'var(--vl-blue)', width: `${classifyProgress.total > 0 ? (classifyProgress.current / classifyProgress.total * 100) : 0}%`, transition: 'width 0.3s' }} />
                  </div>
                  <div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>Dokument {classifyProgress.current} von {classifyProgress.total} wird analysiert…</div>
                </>
              ) : (
                <>
                  <div style={{ fontSize: 14, color: 'var(--danger)', marginBottom: 'var(--space-4)' }}>
                    Verbindung unterbrochen: {error}
                  </div>
                  <button className="btn btn-primary" onClick={() => { setError(null); startClassification(); }}>
                    Erneut versuchen
                  </button>
                  <button className="btn btn-ghost" onClick={() => { setError(null); setPhase('select'); }} style={{ marginLeft: 8 }}>
                    Zurück
                  </button>
                </>
              )}
            </div>
          )}

          {/* ── Phase: Typ-Review ── */}
          {phase === 'review' && (
            <div style={{ overflowX: 'auto' }}>
              <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
                <thead>
                  <tr>
                    <th style={{ padding: '8px 6px', textAlign: 'left', fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', borderBottom: '2px solid var(--border-light)' }}>Datei</th>
                    <th style={{ padding: '8px 6px', textAlign: 'left', fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', borderBottom: '2px solid var(--border-light)', minWidth: 180 }}>Erkannter Typ</th>
                    {allObjekte.length > 1 && <th style={{ padding: '8px 6px', textAlign: 'left', fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', borderBottom: '2px solid var(--border-light)', minWidth: 120 }}>Objekt</th>}
                  </tr>
                </thead>
                <tbody>
                  {classifications.map(c => (
                    <tr key={c.index} style={{ borderBottom: '1px solid var(--border-light)' }}>
                      <td style={{ padding: '8px 6px', maxWidth: 180 }}>
                        <div style={{ fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.filename}</div>
                        {c.reasoning && <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2 }}>{c.reasoning}</div>}
                      </td>
                      <td style={{ padding: '8px 6px' }}>
                        <select value={c.typ_id} onChange={e => updateClassification(c.index, 'typ_id', e.target.value)}
                          style={{ width: '100%', padding: '6px 8px', fontSize: 12, border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)', background: 'var(--surface)', outline: 'none' }}>
                          {allTypes.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
                        </select>
                      </td>
                      {allObjekte.length > 1 && (
                        <td style={{ padding: '8px 6px' }}>
                          {(c.scope === 'objekt' || c.scope === 'gutachten') ? (
                            <select value={c.objekt_id || ''} onChange={e => updateClassification(c.index, 'objekt_id', e.target.value || null)}
                              style={{ width: '100%', padding: '6px 8px', fontSize: 12, border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)', background: 'var(--surface)', outline: 'none' }}>
                              <option value="">— alle —</option>
                              {allObjekte.map(o => <option key={o.id} value={o.id}>{o.bezeichnung || o.objekttyp || 'Objekt'}</option>)}
                            </select>
                          ) : <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Auftrag</span>}
                        </td>
                      )}
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}

          {/* ── Phase: Upload + Extraktion ── */}
          {phase === 'uploading' && (
            <div>
              {classifications.map(c => {
                const prog = uploadProgress[c.index] || {};
                return (
                  <div key={c.index} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', borderBottom: '1px solid var(--border-light)' }}>
                    <span style={{ fontSize: 16, width: 24, textAlign: 'center', color: prog.step === 'done' ? 'var(--success)' : prog.step === 'error' ? 'var(--danger)' : 'var(--text-tertiary)' }}>
                      {prog.step === 'done' ? '✓' : prog.step === 'error' ? '✗' : prog.step === 'waiting' ? '⏳' : '⟳'}
                    </span>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 13, fontWeight: 500 }}>{c.filename}</div>
                      <div style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                        {c.typ_label}
                        {prog.step === 'error' && <span style={{ color: 'var(--danger)' }}> — {prog.error}</span>}
                      </div>
                    </div>
                    {prog.step && !['waiting', 'done', 'error'].includes(prog.step) && (
                      <div style={{ width: 60, height: 4, background: 'var(--border-light)', borderRadius: 2, overflow: 'hidden' }}>
                        <div style={{ height: '100%', width: `${prog.pct || 0}%`, background: 'var(--vl-blue)', transition: 'width 0.3s' }} />
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          )}

          {/* ── Phase: Feld-Review — PDF-Viewer (direkt im modal-body für korrektes Flex-Layout) ── */}
          {phase === 'field_review' && reviewPdfUrl && (
            <div style={{ flex: '0 0 48%', borderRight: '1px solid var(--border-light)', display: 'flex', flexDirection: 'column', minHeight: 500 }}>
              <iframe src={reviewPdfUrl} style={{ width: '100%', flex: 1, border: 'none' }} title="PDF-Vorschau" />
            </div>
          )}

          {/* ── Phase: Feld-Review — Felder-Liste ── */}
          {phase === 'field_review' && (
            <div style={{ flex: 1, overflowY: 'auto', minHeight: 0, maxHeight: reviewPdfUrl ? '100%' : 'none' }}>
              {extractionResults.map((r, rIdx) => {
                const isOpen = openDocIdx === rIdx;
                const hasFields = r.scalarKeys?.length > 0;
                const hasComplex = r.complexKeys?.length > 0;
                const hasAny = hasFields || hasComplex || r.beteiligteArr?.length > 0;
                const acceptedCount = (r.accepted?.size || 0) + (r.acceptedBet?.size || 0);
                if (r.error) {
                  return (
                    <div key={r.index} style={{ padding: '10px 12px', borderBottom: '1px solid var(--border-light)' }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                        <div style={{ flex: 1 }}>
                          <div style={{ fontSize: 13, fontWeight: 500 }}>{r.filename}</div>
                          <div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 2 }}>Fehler: {r.error}</div>
                        </div>
                        <button
                          type="button"
                          className="btn btn-sm"
                          disabled={retryingIdx === rIdx}
                          onClick={() => retryExtraction(rIdx)}
                          style={{ whiteSpace: 'nowrap', fontSize: 12 }}
                        >
                          {retryingIdx === rIdx ? 'Wird wiederholt…' : 'Erneut versuchen'}
                        </button>
                      </div>
                    </div>
                  );
                }
                return (
                  <div key={r.index} style={{ borderBottom: '1px solid var(--border-light)' }}>
                    <button type="button" onClick={() => openReviewDoc(rIdx)} style={{
                      display: 'flex', alignItems: 'center', gap: 10, width: '100%',
                      padding: '10px 12px', background: isOpen ? 'var(--surface-light, #f8f8f8)' : 'none', border: 'none', cursor: 'pointer', textAlign: 'left',
                    }}>
                      <span style={{ fontSize: 14, color: 'var(--text-tertiary)', transform: isOpen ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.2s', display: 'inline-block' }}>▸</span>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 13, fontWeight: 600 }}>{r.filename}</div>
                        <div style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                          {r.typ_label}
                          {hasFields && <span style={{ color: 'var(--success)' }}> — {acceptedCount} von {(r.scalarKeys?.length || 0) + (r.beteiligteArr?.length || 0)} ausgewählt</span>}
                          {!hasAny && ' — nur abgelegt (kein Extraktions-Prompt)'}
                        </div>
                      </div>
                      {hasFields && (
                        <span style={{
                          fontSize: 11, fontWeight: 700, background: acceptedCount > 0 ? 'var(--vl-blue)' : 'var(--border-light)',
                          color: acceptedCount > 0 ? '#fff' : 'var(--text-tertiary)', padding: '2px 8px', borderRadius: 10,
                        }}>{acceptedCount}</span>
                      )}
                    </button>

                    {isOpen && hasAny && (
                      <div style={{ padding: '0 12px 12px 36px' }}>
                        {/* Alle an/ab */}
                        {hasFields && (
                          <button type="button" onClick={() => toggleAllFields(rIdx)} style={{
                            fontSize: 11, color: 'var(--vl-blue-light)', background: 'none', border: 'none', cursor: 'pointer', marginBottom: 8, padding: 0,
                          }}>
                            {r.scalarKeys.every(k => r.accepted.has(k)) ? 'Alle abwählen' : 'Alle auswählen'}
                          </button>
                        )}

                        {/* Felder — bei bekanntem Doc-Typ alle SOLL-Felder (auch nicht extrahierte),
                            sonst nur extrahierte Felder (Fallback). Manuelle Eingabe in leeren
                            Feldern wird automatisch akzeptiert. */}
                        {(() => {
                          const spec = getDocSpecForType(r.typ_id);
                          if (spec) {
                            const allValues = { ...(r.fields || {}), ...(r.manualValues || {}) };
                            const visibleFields = spec.fields.filter(f => docFieldVisible(f.condition, allValues));
                            const extractedCount = visibleFields.filter(f => {
                              const v = r.fields?.[f.id];
                              return v !== undefined && v !== null && v !== '';
                            }).length;
                            const missingCount = visibleFields.length - extractedCount;
                            return (
                              <>
                                {missingCount > 0 && (
                                  <div style={{
                                    padding: '6px 8px', fontSize: 11, color: 'var(--text-secondary)',
                                    background: 'rgba(234,179,8,0.08)', borderRadius: 'var(--radius-sm)',
                                    marginBottom: 6,
                                  }}>
                                    KI hat <strong>{extractedCount} von {visibleFields.length}</strong> SOLL-Feldern
                                    extrahiert. Die <strong>{missingCount} fehlenden</strong> Felder kannst du manuell
                                    aus dem Dokument übernehmen (Felder mit gestricheltem Rand).
                                  </div>
                                )}
                                {visibleFields.map(field => {
                                  const fieldKey = field.id;
                                  const extracted = r.fields?.[fieldKey];
                                  const manual = r.manualValues?.[fieldKey];
                                  const isExtracted = extracted !== undefined && extracted !== null && extracted !== '';
                                  const value = manual !== undefined ? manual : (isExtracted ? extracted : '');
                                  const hasValue = value !== undefined && value !== null && value !== '';
                                  const checked = r.accepted.has(fieldKey);
                                  const fieldType = field.type || 'phrase';
                                  return (
                                    <div key={fieldKey} style={{
                                      display: 'flex', alignItems: 'center', gap: 8,
                                      padding: '5px 8px', borderRadius: 'var(--radius-sm)',
                                      background: checked
                                        ? 'var(--success-bg, rgba(34,197,94,0.06))'
                                        : (isExtracted ? 'transparent' : 'rgba(234,179,8,0.04)'),
                                      fontSize: 13, marginBottom: 2,
                                    }}>
                                      <input type="checkbox"
                                        checked={checked}
                                        onChange={() => toggleField(rIdx, fieldKey)}
                                        disabled={!hasValue}
                                        style={{ accentColor: 'var(--success)', cursor: hasValue ? 'pointer' : 'not-allowed', flexShrink: 0 }} />
                                      <span style={{ color: 'var(--text-secondary)', minWidth: 160, fontSize: 12, flexShrink: 0 }}>
                                        {field.label}{field.required && <span style={{ color: 'var(--danger)' }}> *</span>}
                                        {!isExtracted && hasValue && (
                                          <span style={{ marginLeft: 6, fontSize: 10, color: 'var(--vl-blue)', fontWeight: 600 }}>manuell</span>
                                        )}
                                      </span>
                                      {fieldType === 'enum' && field.options ? (
                                        <select
                                          value={value || ''}
                                          onChange={(e) => setManualValue(rIdx, fieldKey, e.target.value)}
                                          style={{
                                            flex: 1, border: isExtracted ? '1px solid transparent' : '1px dashed var(--border-medium)',
                                            background: isExtracted ? 'transparent' : 'var(--surface)',
                                            padding: '3px 6px', fontSize: 13, borderRadius: 'var(--radius-sm)',
                                            color: hasValue ? 'var(--text-primary)' : 'var(--text-tertiary)',
                                          }}
                                        >
                                          <option value="">— bitte wählen —</option>
                                          {field.options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
                                        </select>
                                      ) : fieldType === 'boolean' ? (
                                        <select
                                          value={value === true || value === 'true' ? 'true' : value === false || value === 'false' ? 'false' : ''}
                                          onChange={(e) => setManualValue(rIdx, fieldKey, e.target.value === 'true' ? true : e.target.value === 'false' ? false : null)}
                                          style={{
                                            flex: 1, border: isExtracted ? '1px solid transparent' : '1px dashed var(--border-medium)',
                                            background: isExtracted ? 'transparent' : 'var(--surface)',
                                            padding: '3px 6px', fontSize: 13, borderRadius: 'var(--radius-sm)',
                                          }}
                                        >
                                          <option value="">—</option>
                                          <option value="true">Ja</option>
                                          <option value="false">Nein</option>
                                        </select>
                                      ) : fieldType === 'text' ? (
                                        <textarea
                                          value={value || ''}
                                          onChange={(e) => setManualValue(rIdx, fieldKey, e.target.value)}
                                          placeholder={field.placeholder || 'manuell ergänzen…'}
                                          rows={2}
                                          style={{
                                            flex: 1, border: isExtracted ? '1px solid transparent' : '1px dashed var(--border-medium)',
                                            background: isExtracted ? 'transparent' : 'var(--surface)',
                                            padding: '4px 6px', fontSize: 13, borderRadius: 'var(--radius-sm)',
                                            fontFamily: 'inherit', resize: 'vertical', minHeight: 32,
                                          }}
                                        />
                                      ) : (
                                        <input
                                          type={fieldType === 'date' ? 'date' : fieldType === 'number' || fieldType === 'currency' ? 'text' : 'text'}
                                          value={value === true ? 'Ja' : value === false ? 'Nein' : (value ?? '')}
                                          onChange={(e) => setManualValue(rIdx, fieldKey, e.target.value)}
                                          placeholder={field.placeholder || (isExtracted ? '' : 'manuell ergänzen…')}
                                          style={{
                                            flex: 1, border: isExtracted ? '1px solid transparent' : '1px dashed var(--border-medium)',
                                            background: isExtracted ? 'transparent' : 'var(--surface)',
                                            padding: '4px 6px', fontSize: 13, borderRadius: 'var(--radius-sm)',
                                            color: hasValue ? 'var(--text-primary)' : 'var(--text-tertiary)',
                                          }}
                                        />
                                      )}
                                    </div>
                                  );
                                })}
                              </>
                            );
                          }
                          // Fallback: nur extrahierte Felder (für Doc-Typen ohne SOLL-Spec)
                          return r.scalarKeys.map(key => {
                            const val = r.fields[key];
                            const checked = r.accepted.has(key);
                            return (
                              <label key={key} style={{
                                display: 'flex', alignItems: 'center', gap: 8,
                                padding: '5px 8px', borderRadius: 'var(--radius-sm)',
                                background: checked ? 'var(--success-bg, rgba(34,197,94,0.06))' : 'transparent',
                                cursor: 'pointer', fontSize: 13, marginBottom: 2,
                              }}>
                                <input type="checkbox" checked={checked} onChange={() => toggleField(rIdx, key)}
                                  style={{ accentColor: 'var(--success)', cursor: 'pointer', flexShrink: 0 }} />
                                <span style={{ color: 'var(--text-secondary)', minWidth: 140, fontSize: 12, flexShrink: 0 }}>{FELD_LABELS[key] || key}</span>
                                <span style={{ fontWeight: 500, color: checked ? 'var(--text-primary)' : 'var(--text-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                                  {val === true ? 'Ja' : val === false ? 'Nein' : String(val).substring(0, 80)}
                                </span>
                              </label>
                            );
                          });
                        })()}

                        {/* Komplexe Felder (Arrays) — nur Info, nicht checkbar */}
                        {r.complexKeys?.map(key => {
                          const arr = r.fields[key];
                          const label = FELD_LABELS[key] || key.replace(/_/g, ' ');
                          return (
                            <div key={key} style={{
                              padding: '5px 8px', fontSize: 12, color: 'var(--text-secondary)',
                              marginBottom: 2, background: 'var(--surface-light, #f8f8f8)', borderRadius: 'var(--radius-sm)',
                            }}>
                              <span style={{ fontWeight: 600 }}>{label}</span>: {arr.length} {arr.length === 1 ? 'Eintrag' : 'Einträge'}
                              {arr.slice(0, 2).map((entry, eIdx) => (
                                <div key={eIdx} style={{ marginLeft: 12, marginTop: 2, fontSize: 11, color: 'var(--text-tertiary)' }}>
                                  {Object.entries(entry).filter(([, v]) => v != null && v !== '' && v !== false).slice(0, 4).map(([k, v]) => `${k}: ${v}`).join(', ')}
                                </div>
                              ))}
                              {arr.length > 2 && <div style={{ marginLeft: 12, fontSize: 11, color: 'var(--text-tertiary)' }}>… und {arr.length - 2} weitere</div>}
                            </div>
                          );
                        })}

                        {/* Beteiligte */}
                        {r.beteiligteArr.length > 0 && (
                          <div style={{ marginTop: 8 }}>
                            <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', marginBottom: 4 }}>Beteiligte ({r.beteiligteArr.length})</div>
                            {r.beteiligteArr.map((b, bIdx) => (
                              <label key={bIdx} style={{
                                display: 'flex', alignItems: 'center', gap: 8, padding: '4px 8px',
                                borderRadius: 'var(--radius-sm)', fontSize: 12, cursor: 'pointer',
                                background: r.acceptedBet?.has(bIdx) ? 'var(--success-bg, rgba(34,197,94,0.06))' : 'transparent',
                              }}>
                                <input type="checkbox" checked={r.acceptedBet?.has(bIdx) || false}
                                  onChange={() => toggleBet(rIdx, bIdx)}
                                  style={{ accentColor: 'var(--success)', cursor: 'pointer' }} />
                                <span style={{ fontWeight: 600 }}>{b.rolle}:</span> {b.name}
                                {b.anschrift && <span style={{ color: 'var(--text-tertiary)' }}> — {b.anschrift}</span>}
                              </label>
                            ))}
                          </div>
                        )}
                      </div>
                    )}
                  </div>
                );
              })}
              </div>
          )}

          {/* ── Phase: Applying ── */}
          {phase === 'applying' && (
            <div style={{ textAlign: 'center', padding: 'var(--space-6)' }}>
              <div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>Übernehme Daten…</div>
            </div>
          )}

          {/* ── Phase: Done ── */}
          {phase === 'done' && applySummary && (
            <div style={{ padding: 'var(--space-5)', textAlign: 'center' }}>
              <div style={{ fontSize: 32, marginBottom: 'var(--space-3)' }}>{applySummary.applied > 0 ? '✓' : '📁'}</div>
              <div style={{ fontSize: 16, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 4 }}>
                {applySummary.applied > 0 ? `${applySummary.applied} Felder übernommen` : `${applySummary.total} Dokumente abgelegt`}
              </div>
              <div style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Die Unterlagen sind jetzt im Auftrag verfügbar.</div>
            </div>
          )}
        </div>

        <div className="modal-footer" style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-3)' }}>
          {phase === 'select' && (
            <>
              <button className="btn btn-ghost" onClick={onClose}>Abbrechen</button>
              <button className="btn btn-primary" disabled={files.length === 0} onClick={startClassification}>
                {files.length > 0 ? `${files.length} Dateien erkennen` : 'Dateien auswählen'}
              </button>
            </>
          )}
          {phase === 'review' && (
            <>
              <button className="btn btn-ghost" onClick={() => { setPhase('select'); setClassifications([]); }}>Zurück</button>
              <button className="btn btn-primary" onClick={startProcessing}>{classifications.length} Dokumente hochladen</button>
            </>
          )}
          {phase === 'field_review' && (
            <>
              <button className="btn btn-ghost" onClick={onClose}>Abbrechen</button>
              <button className="btn btn-primary" disabled={totalAccepted === 0} onClick={startApply}>
                {totalAccepted > 0 ? `${totalAccepted} Felder übernehmen` : 'Keine Felder ausgewählt'}
              </button>
            </>
          )}
          {phase === 'done' && (
            <button className="btn btn-primary" onClick={onClose}>Schließen</button>
          )}
        </div>
      </div>
    </div>
  );
};


const NeuerAuftragModal = ({ onClose, onCreated, session, workerUrl, userProfile, orgMembers }) => {
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  // Modus: KI-Erfassung (PDF hochladen) oder Manuelle Erfassung (Wizard).
  // KI ist der historische Default — Manuell ist für Privat-/Familiengericht-
  // Aufträge wo keine maschinenlesbaren Beschlüsse/Anschreiben vorliegen.
  const [modus, setModus] = useState('ki'); // 'ki' | 'manuell'

  // Form-State (KI-Variante)
  const [auftragsart, setAuftragsart] = useState('gericht');
  const [auftragstyp, setAuftragstyp] = useState(APP_MODE === 'bauschaden' ? 'bauschaden' : 'verkehrswert');

  // Form-State (Manuelle Variante — Wizard-Felder)
  const [mAuftragsart, setMAuftragsart] = useState(APP_MODE === 'bauschaden' ? 'gericht' : 'privat');
  const [mAuftragstyp, setMAuftragstyp] = useState(APP_MODE === 'bauschaden' ? 'bauschaden' : 'verkehrswert');
  const [mAuftraggeber, setMAuftraggeber] = useState('');     // Name + Adresse (mehrzeilig)
  const [mObjektAdresse, setMObjektAdresse] = useState('');   // Straße + PLZ + Ort
  const [mAbgabeExtern, setMAbgabeExtern] = useState('');     // Frist (ISO yyyy-mm-dd)
  // Sachverständiger: ID des Org-Mitglieds. Sein akte_kuerzel bestimmt das
  // Aktenzeichen. Default = eingeloggter User, aber NUR wenn er selbst SV ist
  // (akte_kuerzel hinterlegt). Sachbearbeiter müssen aktiv einen SV wählen.
  const [mSachverstaendigerId, setMSachverstaendigerId] = useState(
    userProfile?.akte_kuerzel ? (userProfile?.id || '') : ''
  );
  const [mFile, setMFile] = useState(null);          // optionales PDF-Anhang
  const [mFileError, setMFileError] = useState(null);// Datei-Validation
  const manualFileInputRef = useRef(null);
  const [mDragOver, setMDragOver] = useState(false);

  // Falls userProfile erst nach dem Mount lädt (z.B. direkt nach Login):
  // Default-SV nur setzen wenn der eingeloggte User selbst SV ist (Kürzel hat).
  // Sachbearbeiter sehen den Empty-State und müssen aktiv wählen.
  useEffect(() => {
    if (userProfile?.id && userProfile?.akte_kuerzel && !mSachverstaendigerId) {
      setMSachverstaendigerId(userProfile.id);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userProfile?.id, userProfile?.akte_kuerzel]);

  // Dateien (beliebig viele PDFs)
  const [files, setFiles] = useState([]); // [{file, typ}]
  const fileInputRef = useRef(null);
  const [dragOver, setDragOver] = useState(false);

  // Dokumenttyp-Erkennung aus Dateiname
  const detectType = (file) => {
    const n = file.name.toLowerCase();
    if (n.includes('beschl') || n.includes('beweisbeschl') || n.includes('anordnung')) return APP_MODE === 'bauschaden' ? 'beweisbeschluss' : 'gerichtsbeschluss';
    if (n.includes('anschreib') || n.includes('begleit') || n.includes('auftrag') || n.includes('schreiben')) return 'anschreiben';
    if (n.includes('grundbuch')) return 'grundbuchauszug';
    if (n.includes('teilung')) return 'teilungserklaerung';
    if (n.includes('flurkarte') || n.includes('lageplan')) return 'flurkarte';
    return null;
  };

  const addFiles = (newFiles) => {
    const pdfs = Array.from(newFiles).filter(f => f.type === 'application/pdf' || f.name.endsWith('.pdf'));
    if (pdfs.length === 0) return;
    const BESCHLUSS_TYP = APP_MODE === 'bauschaden' ? 'beweisbeschluss' : 'gerichtsbeschluss';
    const classified = pdfs.map(f => ({ file: f, typ: detectType(f) }));
    const hasBeschluss = files.some(c => c.typ === BESCHLUSS_TYP) || classified.some(c => c.typ === BESCHLUSS_TYP);
    if (!hasBeschluss) {
      const unknowns = classified.filter(c => !c.typ);
      if (unknowns.length > 1) {
        // Mehrere Dateien ohne Erkennung: größte = Beschluss, Rest = Anschreiben
        unknowns.sort((a, b) => b.file.size - a.file.size);
        unknowns[0].typ = BESCHLUSS_TYP;
        unknowns.slice(1).forEach(c => { if (!c.typ) c.typ = 'anschreiben'; });
      } else if (unknowns.length === 1) {
        // Nur eine Datei ohne Erkennung: als Anschreiben typen (nicht Beschluss annehmen)
        unknowns[0].typ = 'anschreiben';
      }
    } else {
      classified.forEach(c => { if (!c.typ) c.typ = 'anschreiben'; });
    }
    setFiles(prev => [...prev, ...classified]);
  };

  const removeFile = (idx) => setFiles(prev => prev.filter((_, i) => i !== idx));

  const handleSubmit = async () => {
    if (saving) return;

    setSaving(true);
    setError(null);
    try {
      if (modus === 'manuell') {
        // ── Manueller Wizard-Pfad ───────────────────────────────
        // Validation
        if (!mAuftraggeber.trim()) {
          throw new Error('Bitte Auftraggeber angeben');
        }
        // Ausgewählten SV-Member auflösen (Name + Kürzel fürs Aktenzeichen)
        const sv = (orgMembers || []).find(m => m.id === mSachverstaendigerId);
        const projektId = await createProjekt({
          auftragsart: mAuftragsart,
          auftragstyp: mAuftragstyp,
          auftraggeber: mAuftraggeber.trim(),
          erstesGutachtenAdresse: mObjektAdresse.trim() || '',
          abgabeExtern: mAbgabeExtern || null,
          sachverstaendiger: sv?.name || null,
          sachverstaendigerKuerzel: sv?.akte_kuerzel || null,
          kiFlow: false,
        });

        // Optional: PDF-Anhang hochladen (non-extractable → kein KI-Call,
        // landet als "Sonstiger Beschluss" in Auftragsunterlagen). User kann
        // den Titel im UnterlagenTab manuell anpassen.
        if (mFile) {
          try {
            await bulkExtractDoc(
              mFile, 'sonstiger_beschluss', 'auftrag',
              projektId, null, null,
              session, workerUrl,
              () => {}
            );
          } catch (uploadErr) {
            // Upload-Fehler ist nicht fatal: Auftrag ist bereits angelegt.
            // User sieht den Fehler kurz, kann dann das Dokument manuell hochladen.
            console.warn('[NeuerAuftrag] PDF-Upload fehlgeschlagen:', uploadErr);
            setError(`Auftrag wurde angelegt, aber PDF-Upload schlug fehl: ${uploadErr.message || uploadErr}. Bitte später manuell im Unterlagen-Tab hochladen.`);
            // Wir navigieren trotzdem ins neue Projekt — der Auftrag steht
            onCreated(projektId, { kiFiles: [] });
            return;
          }
        }

        onCreated(projektId, { kiFiles: [] });
        return;
      }

      // ── KI-Pfad (PDF-Upload, bestehend) ─────────────────────
      const projektId = await createProjekt({
        auftragsart: files.some(f => f.typ === 'gerichtsbeschluss' || f.typ === 'beweisbeschluss') ? 'gericht' : auftragsart,
        auftragstyp,
        kiFlow: files.length > 0,
      });
      const sorted = [...files].sort((a, b) => {
        if (a.typ === 'gerichtsbeschluss') return -1;
        if (b.typ === 'gerichtsbeschluss') return 1;
        return 0;
      });
      const kiFiles = sorted.map(c => ({ file: c.file, typ: c.typ || 'anschreiben' }));
      onCreated(projektId, { kiFiles });
    } catch (err) {
      setError(err.message || String(err));
      setSaving(false);
    }
  };

  const typLabels = {
    gerichtsbeschluss: 'Beweisbeschluss', anschreiben: 'Auftrag',
    grundbuchauszug: 'Grundbuch', teilungserklaerung: 'Teilungserkl.',
    flurkarte: 'Flurkarte',
  };

  const inputStyle = {
    width: '100%', padding: '10px 12px',
    border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
    fontSize: 14, fontFamily: 'inherit', outline: 'none', background: 'var(--surface)',
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 520 }}>
        <div className="modal-header">
          <div><div className="modal-title">Neuer Auftrag</div></div>
          <button className="modal-close" onClick={onClose}>×</button>
        </div>

        <div className="modal-body" style={{ padding: 'var(--space-5)' }}>
          {/* ── Modus-Toggle: KI vs Manuell ── */}
          <div style={{
            display: 'flex', gap: 2, marginBottom: 'var(--space-4)',
            background: 'var(--surface-light)', padding: 4,
            borderRadius: 'var(--radius-md)',
          }}>
            <button type="button" onClick={() => setModus('ki')}
              style={{
                flex: 1, padding: '8px 12px',
                background: modus === 'ki' ? 'var(--surface)' : 'transparent',
                color: modus === 'ki' ? 'var(--text-primary)' : 'var(--text-secondary)',
                border: 'none', borderRadius: 'var(--radius-sm)',
                fontSize: 13, fontWeight: modus === 'ki' ? 600 : 500,
                cursor: 'pointer',
                boxShadow: modus === 'ki' ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
              }}>
              KI-Erfassung (PDF)
            </button>
            <button type="button" onClick={() => setModus('manuell')}
              style={{
                flex: 1, padding: '8px 12px',
                background: modus === 'manuell' ? 'var(--surface)' : 'transparent',
                color: modus === 'manuell' ? 'var(--text-primary)' : 'var(--text-secondary)',
                border: 'none', borderRadius: 'var(--radius-sm)',
                fontSize: 13, fontWeight: modus === 'manuell' ? 600 : 500,
                cursor: 'pointer',
                boxShadow: modus === 'manuell' ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
              }}>
              Manuell anlegen
            </button>
          </div>

          {modus === 'ki' && (<>
          {/* Drop-Zone */}
          <div
            onDragOver={e => { e.preventDefault(); setDragOver(true); }}
            onDragLeave={() => setDragOver(false)}
            onDrop={e => { e.preventDefault(); setDragOver(false); addFiles(e.dataTransfer.files); }}
            onClick={() => fileInputRef.current?.click()}
            style={{
              border: `2px dashed ${dragOver ? 'var(--vl-blue)' : 'var(--border-light)'}`,
              borderRadius: 'var(--radius-lg)',
              padding: files.length > 0 ? '16px 20px' : '32px 20px',
              textAlign: 'center', cursor: 'pointer',
              background: dragOver ? 'rgba(37, 99, 235, 0.04)' : 'var(--surface-light)',
              transition: 'all 0.15s ease', marginBottom: 'var(--space-4)',
            }}
          >
            <input
              ref={fileInputRef} type="file" accept=".pdf,application/pdf" multiple
              style={{ display: 'none' }}
              onChange={e => { addFiles(e.target.files); e.target.value = ''; }}
            />
            {files.length === 0 ? (
              <>
                <div style={{ fontSize: 28, marginBottom: 8, opacity: 0.3 }}><IconDocument size={28} /></div>
                <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 4 }}>
                  Beweisbeschluss und Auftragsunterlagen hierher ziehen
                </div>
                <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                  oder klicken zum Auswählen — PDF-Dateien
                </div>
              </>
            ) : (
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center' }}>
                {files.map((f, i) => (
                  <div key={i} style={{
                    display: 'flex', alignItems: 'center', gap: 6,
                    padding: '6px 10px', background: 'white',
                    border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)', fontSize: 12,
                  }}>
                    <IconDocument size={14} />
                    <span style={{ fontWeight: 500, maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                      {f.file.name}
                    </span>
                    <span style={{ color: 'var(--vl-blue)', fontSize: 11 }}>{typLabels[f.typ] || 'Dokument'}</span>
                    <button type="button" onClick={e => { e.stopPropagation(); removeFile(i); }}
                      style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: 'var(--text-tertiary)', fontSize: 14, lineHeight: 1 }}>×</button>
                  </div>
                ))}
                <div style={{ fontSize: 11, color: 'var(--text-tertiary)', alignSelf: 'center' }}>+ weitere</div>
              </div>
            )}
          </div>

          {/* Aktenzeichen (intern) — automatisch vom System vergeben */}
          <div style={{ marginBottom: 'var(--space-4)', padding: '10px 14px', background: 'var(--surface-light)', borderRadius: 'var(--radius-sm)', fontSize: 12, color: 'var(--text-secondary)' }}>
            Internes Aktenzeichen wird automatisch vergeben.
          </div>

          {/* Erweiterte Optionen — nur ohne Dateien sinnvoll, mit Dateien wird alles auto-erkannt */}
          {files.length === 0 && (
            <div style={{ marginBottom: 'var(--space-3)' }}>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
                <div>
                  <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>Auftragsart</label>
                  <select value={auftragsart} onChange={e => setAuftragsart(e.target.value)} style={{ ...inputStyle, fontSize: 13 }}>
                    <option value="gericht">Gerichtsauftrag</option>
                    <option value="privat">Privatauftrag</option>
                    <option value="gutachterausschuss">Gutachterausschuss</option>
                    <option value="notariat">Notarauftrag</option>
                  </select>
                </div>
                <div>
                  <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>Auftragstyp</label>
                  <select value={auftragstyp} onChange={e => setAuftragstyp(e.target.value)} style={{ ...inputStyle, fontSize: 13 }}>
                    {APP_MODE === 'bauschaden' ? (
                      <option value="bauschaden">Bauschadengutachten</option>
                    ) : (<>
                      <option value="verkehrswert">Verkehrswertgutachten</option>
                      <option value="miete">Mietwertgutachten</option>
                      <option value="beleihung">Beleihungswertgutachten</option>
                    </>)}
                  </select>
                </div>
              </div>
            </div>
          )}
          </>)}

          {/* ── Manueller Wizard ── */}
          {modus === 'manuell' && (
            <div>
              <div style={{
                padding: '10px 14px', marginBottom: 'var(--space-4)',
                background: 'rgba(37, 99, 235, 0.04)',
                border: '1px solid rgba(37, 99, 235, 0.15)',
                borderRadius: 'var(--radius-sm)',
                fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.5,
              }}>
                Für Aufträge ohne maschinenlesbaren Beweisbeschluss (z.B. Privatauftrag,
                Landgericht/Familiengericht). Alle Felder können später im StammdatenTab
                ergänzt oder korrigiert werden.
              </div>

              {/* Auftragsart / Auftragstyp */}
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
                <div>
                  <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>Auftragsart *</label>
                  <select value={mAuftragsart} onChange={e => setMAuftragsart(e.target.value)} style={{ ...inputStyle, fontSize: 13 }}>
                    <option value="privat">Privatauftrag</option>
                    <option value="gericht">Gerichtsauftrag (Landgericht, Familiengericht, …)</option>
                    <option value="gutachterausschuss">Gutachterausschuss</option>
                    <option value="notariat">Notarauftrag</option>
                  </select>
                </div>
                <div>
                  <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>Auftragstyp *</label>
                  <select value={mAuftragstyp} onChange={e => setMAuftragstyp(e.target.value)} style={{ ...inputStyle, fontSize: 13 }}>
                    {APP_MODE === 'bauschaden' ? (
                      <option value="bauschaden">Bauschadengutachten</option>
                    ) : (<>
                      <option value="verkehrswert">Verkehrswertgutachten</option>
                      <option value="miete">Mietwertgutachten</option>
                      <option value="beleihung">Beleihungswertgutachten</option>
                      <option value="marktwert">Marktwertindikation</option>
                      <option value="minderwert">Minderwertgutachten</option>
                      <option value="ueberwachung">Überwachungsgutachten</option>
                    </>)}
                  </select>
                </div>
              </div>

              {/* Auftraggeber */}
              <div style={{ marginBottom: 'var(--space-3)' }}>
                <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>
                  Auftraggeber * <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(Name und Adresse, ggf. mehrzeilig)</span>
                </label>
                <textarea
                  value={mAuftraggeber}
                  onChange={e => setMAuftraggeber(e.target.value)}
                  placeholder={"z.B.\nFrau Sandra Weber\nLehmusstraße 32\n90766 Fürth"}
                  rows={4}
                  style={{ ...inputStyle, fontSize: 13, resize: 'vertical', minHeight: 80, fontFamily: 'inherit' }}
                />
              </div>

              {/* Objekt-Adresse */}
              <div style={{ marginBottom: 'var(--space-3)' }}>
                <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>
                  Objekt-Adresse <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>(Straße, PLZ Ort)</span>
                </label>
                <input
                  type="text"
                  value={mObjektAdresse}
                  onChange={e => setMObjektAdresse(e.target.value)}
                  placeholder="z.B. Wettersteinstraße 53, 90471 Nürnberg"
                  style={{ ...inputStyle, fontSize: 13 }}
                />
              </div>

              {/* Frist + Sachverständiger */}
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
                <div>
                  <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>
                    Frist (Abgabe extern)
                  </label>
                  <input
                    type="date"
                    value={mAbgabeExtern}
                    onChange={e => setMAbgabeExtern(e.target.value)}
                    style={{ ...inputStyle, fontSize: 13 }}
                  />
                </div>
                <div>
                  <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>
                    Sachverständiger *
                    <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}> — bestimmt das Aktenzeichen-Kürzel</span>
                  </label>
                  {(() => {
                    // Nur Mitglieder mit akte_kuerzel sind Sachverständige.
                    // Sachbearbeiter (kein Kürzel) tauchen im Select nicht auf,
                    // weil sie keine Aufträge als SV anlegen.
                    const sachverstaendige = (orgMembers || []).filter(m => m.akte_kuerzel);
                    if (!orgMembers) {
                      return (
                        <input type="text" value="" disabled placeholder="Lade Mitglieder…"
                          style={{ ...inputStyle, fontSize: 13, color: 'var(--text-tertiary)' }} />
                      );
                    }
                    if (sachverstaendige.length === 0) {
                      return (
                        <div style={{
                          padding: '8px 10px', fontSize: 12,
                          background: 'var(--warning-bg, rgba(234,179,8,0.08))',
                          color: 'var(--warning, #d97706)',
                          borderRadius: 'var(--radius-sm)',
                        }}>
                          Kein Sachverständiger gefunden. Bitte in den Benutzer-Einstellungen
                          für mindestens ein Mitglied ein Aktenzeichen-Kürzel hinterlegen.
                        </div>
                      );
                    }
                    return (
                      <select
                        value={mSachverstaendigerId}
                        onChange={e => setMSachverstaendigerId(e.target.value)}
                        style={{ ...inputStyle, fontSize: 13 }}
                      >
                        <option value="">— bitte wählen —</option>
                        {sachverstaendige.map(m => (
                          <option key={m.id} value={m.id}>
                            {m.name} ({m.akte_kuerzel}){m.isMe ? ' — ich' : ''}
                          </option>
                        ))}
                      </select>
                    );
                  })()}
                </div>
              </div>

              {/* Optionales PDF-Anhang */}
              <div style={{ marginBottom: 'var(--space-3)' }}>
                <label style={{ display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4, color: 'var(--text-tertiary)' }}>
                  Auftragsschreiben (optional) <span style={{ fontWeight: 400, color: 'var(--text-tertiary)' }}>— PDF wird als Beleg im Auftrag abgelegt, keine KI-Auswertung</span>
                </label>
                <div
                  onDragOver={e => { e.preventDefault(); setMDragOver(true); }}
                  onDragLeave={() => setMDragOver(false)}
                  onDrop={e => {
                    e.preventDefault(); setMDragOver(false);
                    const f = Array.from(e.dataTransfer.files).find(x => x.type === 'application/pdf' || x.name.toLowerCase().endsWith('.pdf'));
                    if (f) { setMFile(f); setMFileError(null); }
                    else setMFileError('Nur PDF-Dateien werden akzeptiert.');
                  }}
                  onClick={() => !mFile && manualFileInputRef.current?.click()}
                  style={{
                    border: `2px dashed ${mDragOver ? 'var(--vl-blue)' : 'var(--border-light)'}`,
                    borderRadius: 'var(--radius-sm)',
                    padding: mFile ? '10px 14px' : '20px',
                    textAlign: 'center', cursor: mFile ? 'default' : 'pointer',
                    background: mDragOver ? 'rgba(37, 99, 235, 0.04)' : 'var(--surface-light)',
                    transition: 'all 0.15s ease',
                  }}
                >
                  <input
                    ref={manualFileInputRef} type="file" accept=".pdf,application/pdf"
                    style={{ display: 'none' }}
                    onChange={e => {
                      const f = e.target.files?.[0];
                      if (f) { setMFile(f); setMFileError(null); }
                      e.target.value = '';
                    }}
                  />
                  {mFile ? (
                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, fontSize: 13 }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
                        <IconDocument size={14} />
                        <span style={{ fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                          {mFile.name}
                        </span>
                        <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                          ({(mFile.size / 1024).toFixed(0)} KB)
                        </span>
                      </div>
                      <button type="button" onClick={(e) => { e.stopPropagation(); setMFile(null); }}
                        style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-tertiary)', fontSize: 16, lineHeight: 1, padding: '2px 6px' }}>
                        ×
                      </button>
                    </div>
                  ) : (
                    <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                      PDF hierher ziehen oder klicken zum Auswählen
                    </div>
                  )}
                </div>
                {mFileError && (
                  <div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 4 }}>{mFileError}</div>
                )}
              </div>
            </div>
          )}

          {error && (
            <div style={{ padding: 'var(--space-3)', background: 'var(--danger-bg)', color: 'var(--danger)', borderRadius: 'var(--radius-sm)', fontSize: 13, marginBottom: 'var(--space-3)' }}>
              {error}
            </div>
          )}
        </div>

        <div style={{
          display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-3)',
          padding: 'var(--space-4) var(--space-5)',
          borderTop: '1px solid var(--border-light)', background: 'var(--surface-light)',
        }}>
          <button className="btn btn-ghost" onClick={onClose} disabled={saving}>Abbrechen</button>
          <button className="btn btn-primary" onClick={handleSubmit}
            disabled={saving || (modus === 'manuell'
              ? (mAuftraggeber.trim().length < 2 || !mSachverstaendigerId)
              : false)}>
            {saving ? 'Wird angelegt…'
              : modus === 'manuell'
                ? 'Auftrag manuell anlegen'
                : files.length > 0
                  ? `Anlegen + ${files.length} Dokument${files.length > 1 ? 'e' : ''} auslesen`
                  : 'Leeren Auftrag anlegen'}
          </button>
        </div>
      </div>
    </div>
  );
};


const ToastContext = React.createContext(() => {});
const useToast = () => React.useContext(ToastContext);

const colorForType = (t) => ({ success: 'var(--success)', error: 'var(--danger)', info: 'var(--vl-blue)' }[t] || 'var(--text-secondary)');
const iconForType = (t) => ({ success: '✓', error: '!', info: 'i' }[t] || 'i');

const ToastProvider = ({ children }) => {
  const [toasts, setToasts] = useState([]);

  const showToast = useCallback((message, type = 'info', duration = 4000) => {
    const id = Date.now() + Math.random();
    setToasts(prev => [...prev, { id, message, type }]);
    setTimeout(() => {
      setToasts(prev => prev.map(t => t.id === id ? { ...t, removing: true } : t));
      setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 350);
    }, duration);
  }, []);

  return (
    <ToastContext.Provider value={showToast}>
      <style>{`
        @keyframes toast-in { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } }
        @keyframes toast-out { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(40px); } }
      `}</style>
      {children}
      {toasts.length > 0 && (
        <div style={{
          position: 'fixed', bottom: 20, right: 20, zIndex: 9999,
          display: 'flex', flexDirection: 'column-reverse', gap: 8,
          pointerEvents: 'none',
        }}>
          {toasts.map(t => (
            <div key={t.id} style={{
              background: 'var(--surface)',
              border: `1px solid ${colorForType(t.type)}`,
              borderLeft: `4px solid ${colorForType(t.type)}`,
              borderRadius: 'var(--radius-md)',
              padding: '10px 16px',
              fontSize: 13, lineHeight: 1.4,
              color: 'var(--text-primary)',
              boxShadow: '0 4px 12px rgba(0,0,0,0.12)',
              maxWidth: 360, minWidth: 220,
              pointerEvents: 'auto',
              animation: t.removing ? 'toast-out 0.3s ease forwards' : 'toast-in 0.3s ease',
              display: 'flex', alignItems: 'flex-start', gap: 8,
            }}>
              <span style={{ color: colorForType(t.type), fontWeight: 700, fontSize: 15, lineHeight: '18px', flexShrink: 0 }}>{iconForType(t.type)}</span>
              <span>{t.message}</span>
            </div>
          ))}
        </div>
      )}
    </ToastContext.Provider>
  );
};

// ══════════════════════════════════════════════════════════════════
// 6 · TOP-LEVEL APP
// ══════════════════════════════════════════════════════════════════

// ────────────────────────────────────────────────────────────────────
// useSessionTimeout — Automatischer Logout nach Inaktivität
// Trackt Maus, Tastatur, Touch. Warnt 5 Min vor Ablauf.
// ────────────────────────────────────────────────────────────────────
const SESSION_TIMEOUT_MS = 8 * 60 * 60 * 1000;   // 8 Stunden
const SESSION_WARNING_MS = 5 * 60 * 1000;         // 5 Minuten vorher warnen

function useSessionTimeout(session, onLogout) {
  const [showWarning, setShowWarning] = useState(false);
  const lastActivityRef = useRef(Date.now());
  const warningTimerRef = useRef(null);
  const logoutTimerRef = useRef(null);

  const resetTimers = useCallback(() => {
    lastActivityRef.current = Date.now();
    setShowWarning(false);

    if (warningTimerRef.current) clearTimeout(warningTimerRef.current);
    if (logoutTimerRef.current) clearTimeout(logoutTimerRef.current);

    if (!session) return;

    warningTimerRef.current = setTimeout(() => {
      setShowWarning(true);
    }, SESSION_TIMEOUT_MS - SESSION_WARNING_MS);

    logoutTimerRef.current = setTimeout(() => {
      onLogout?.();
    }, SESSION_TIMEOUT_MS);
  }, [session, onLogout]);

  useEffect(() => {
    if (!session) return;
    resetTimers();

    const events = ['mousedown', 'keydown', 'touchstart', 'scroll'];
    const handler = () => {
      // Throttle: nur alle 60s die Timer resetten
      if (Date.now() - lastActivityRef.current > 60000) {
        resetTimers();
      }
    };
    events.forEach(e => window.addEventListener(e, handler, { passive: true }));

    return () => {
      events.forEach(e => window.removeEventListener(e, handler));
      if (warningTimerRef.current) clearTimeout(warningTimerRef.current);
      if (logoutTimerRef.current) clearTimeout(logoutTimerRef.current);
    };
  }, [session, resetTimers]);

  return { showWarning, extendSession: resetTimers };
}

// ══════════════════════════════════════════════════════════════════
// 6b · PROJEKT BEFRAGEN — Drawer (rechtes Seitenpanel)
// Slide-out von rechts. Funktioniert aus jeder Projekt-View
// (Dashboard, Auftrag, Gutachten). Jede Frage ist eigenständig:
// Frage stellen → Antwort lesen → "Neue Frage" oder Panel schließen.
// State wird beim Schließen komplett zurückgesetzt.
// ══════════════════════════════════════════════════════════════════

const IconHelpCircle = (p) => <Icon {...p} path={<><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></>} />;
const IconArrowRight = (p) => <Icon {...p} path={<><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></>} />;
const IconRefresh = (p) => <Icon {...p} path={<><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></>} />;

// Schlanker Renderer für die Befragen-Antworten: wandelt das leichte Markdown,
// das Claude typischerweise liefert (**fett**, Aufzählungen, Absätze), in sauberes
// JSX. Bewusst minimal gehalten — keine externe Markdown-Engine, kein HTML-Injection.
const renderInlineBold = (text) => {
  // **fett** → <strong>; teilt den Text an den Markern und rendert abwechselnd
  const parts = String(text).split(/(\*\*[^*]+\*\*)/g);
  return parts.map((p, i) => {
    if (/^\*\*[^*]+\*\*$/.test(p)) {
      return <strong key={i} style={{ fontWeight: 700 }}>{p.slice(2, -2)}</strong>;
    }
    return <React.Fragment key={i}>{p}</React.Fragment>;
  });
};

const AntwortText = ({ text }) => {
  if (!text) return null;
  const zeilen = String(text).replace(/\r/g, '').split('\n');
  const bloecke = [];
  let listenPuffer = [];

  const flushListe = (key) => {
    if (listenPuffer.length === 0) return;
    bloecke.push(
      <ul key={`ul-${key}`} style={{ margin: '4px 0 10px', paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 4 }}>
        {listenPuffer.map((item, i) => (
          <li key={i} style={{ lineHeight: 1.55 }}>{renderInlineBold(item)}</li>
        ))}
      </ul>
    );
    listenPuffer = [];
  };

  zeilen.forEach((zeile, idx) => {
    const t = zeile.trim();
    const listMatch = t.match(/^[-•*]\s+(.*)$/) || t.match(/^\d+\.\s+(.*)$/);
    if (listMatch) {
      listenPuffer.push(listMatch[1]);
    } else if (t === '') {
      flushListe(idx);
    } else {
      flushListe(idx);
      bloecke.push(
        <p key={`p-${idx}`} style={{ margin: '0 0 10px', lineHeight: 1.6 }}>{renderInlineBold(t)}</p>
      );
    }
  });
  flushListe('end');

  return <div>{bloecke}</div>;
};

const QUICK_QUESTIONS = [
  'Wann war der letzte Ortstermin?',
  'Wer ist der Rechtsanwalt?',
  'Welches Aktenzeichen hat der Fall?',
  'Wie groß ist das Grundstück?',
  'Welche Fristen laufen?',
  'Welche Dokumente fehlen noch?',
  'Wer sind die Eigentümer?',
  'Wie ist der Bodenrichtwert?',
];

const ProjektChatDrawer = ({ open, onClose, projektId, projektName, session, workerUrl }) => {
  // Phase: 'idle' → 'loading' → 'answer'
  const [phase, setPhase] = useState('idle');
  const [currentQuestion, setCurrentQuestion] = useState('');
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [input, setInput] = useState('');
  const inputRef = useRef(null);
  const isMobile = useIsMobile();

  // Reset alles wenn Drawer geschlossen wird
  useEffect(() => {
    if (!open) {
      // Kurz warten bis die Slide-Animation fertig ist, dann resetten
      const t = setTimeout(() => {
        setPhase('idle');
        setCurrentQuestion('');
        setAnswer('');
        setError(null);
        setInput('');
      }, 300);
      return () => clearTimeout(t);
    }
  }, [open]);

  // Focus auf Input wenn Drawer öffnet oder nach "Neue Frage"
  useEffect(() => {
    if (open && phase === 'idle' && inputRef.current) {
      const t = setTimeout(() => inputRef.current?.focus(), 350);
      return () => clearTimeout(t);
    }
  }, [open, phase]);

  const askQuestion = useCallback(async (question) => {
    if (!question.trim()) return;

    setCurrentQuestion(question.trim());
    setInput('');
    setPhase('loading');
    setError(null);
    setAnswer('');

    try {
      const res = await fetch(`${workerUrl}/api/project/ask`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${session.access_token}`,
        },
        body: JSON.stringify({ project_id: projektId, question: question.trim() }),
      });

      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error(err.error || `HTTP ${res.status}`);
      }

      const data = await res.json();
      setAnswer(data.answer);
      setPhase('answer');
    } catch (err) {
      setError(err.message || 'Anfrage fehlgeschlagen');
      setPhase('answer');
    }
  }, [projektId, session, workerUrl]);

  const handleKeyDown = useCallback((e) => {
    if (e.key === 'Enter' && !e.shiftKey && input.trim()) {
      e.preventDefault();
      askQuestion(input);
    }
    if (e.key === 'Escape') {
      onClose();
    }
  }, [input, askQuestion, onClose]);

  const resetToIdle = useCallback(() => {
    setPhase('idle');
    setCurrentQuestion('');
    setAnswer('');
    setError(null);
    setInput('');
    setTimeout(() => inputRef.current?.focus(), 50);
  }, []);

  // ─── Render ───

  const drawerWidth = isMobile ? '100vw' : 420;

  return (
    <>
      {/* Backdrop */}
      <div
        onClick={onClose}
        style={{
          position: 'fixed', inset: 0, zIndex: 9990,
          background: 'rgba(0,0,0,0.3)',
          opacity: open ? 1 : 0,
          pointerEvents: open ? 'auto' : 'none',
          transition: 'opacity 0.25s ease',
        }}
      />

      {/* Drawer */}
      <div style={{
        position: 'fixed', top: 0, right: 0, bottom: 0,
        width: drawerWidth, maxWidth: '100vw',
        zIndex: 9991,
        background: 'var(--surface-white, #fff)',
        boxShadow: open ? '-8px 0 30px rgba(0,0,0,0.12)' : 'none',
        transform: open ? 'translateX(0)' : 'translateX(100%)',
        transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
        display: 'flex', flexDirection: 'column',
      }}>

        {/* Header */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '14px 20px', borderBottom: '1px solid var(--border-light)',
          background: 'var(--surface-light)', flexShrink: 0,
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <IconHelpCircle size={18} stroke="var(--vl-blue)" />
            <div>
              <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>
                Auftrag befragen
              </div>
              <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 1 }}>
                {projektName}
              </div>
            </div>
          </div>
          <button
            onClick={onClose}
            style={{
              background: 'none', border: 'none', cursor: 'pointer',
              padding: 6, borderRadius: 'var(--radius-sm)',
              color: 'var(--text-tertiary)', display: 'flex',
            }}
            title="Schließen (Esc)"
          >
            <IconClose size={18} />
          </button>
        </div>

        {/* Content */}
        <div style={{ flex: 1, overflowY: 'auto', padding: '20px' }}>

          {/* Phase: idle — Quick Questions */}
          {phase === 'idle' && (
            <div>
              <div style={{
                fontSize: 13, color: 'var(--text-secondary)',
                marginBottom: 16, lineHeight: 1.5,
              }}>
                Stelle eine Frage zu den Stammdaten, Beteiligten, Fristen oder Dokumenten dieses Auftrags.
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                {QUICK_QUESTIONS.map((q, i) => (
                  <button
                    key={i}
                    onClick={() => askQuestion(q)}
                    style={{
                      display: 'flex', alignItems: 'center', gap: 10,
                      padding: '10px 14px', textAlign: 'left',
                      background: 'var(--surface-light)',
                      border: '1px solid var(--border-light)',
                      borderRadius: 'var(--radius-md, 8px)',
                      cursor: 'pointer', fontSize: 13,
                      color: 'var(--text-primary)',
                      transition: 'all 0.15s',
                    }}
                    onMouseEnter={e => {
                      e.currentTarget.style.borderColor = 'var(--vl-blue)';
                      e.currentTarget.style.background = 'var(--surface-blue, #EEF3F8)';
                    }}
                    onMouseLeave={e => {
                      e.currentTarget.style.borderColor = 'var(--border-light)';
                      e.currentTarget.style.background = 'var(--surface-light)';
                    }}
                  >
                    <span>{q}</span>
                  </button>
                ))}
              </div>
            </div>
          )}

          {/* Phase: loading */}
          {phase === 'loading' && (
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16, paddingTop: 40 }}>
              <div style={{
                fontSize: 13, color: 'var(--text-secondary)',
                padding: '8px 16px', borderRadius: 16,
                background: 'var(--surface-light)', maxWidth: '90%', textAlign: 'center',
              }}>
                {currentQuestion}
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-tertiary)', fontSize: 13 }}>
                <span style={{ display: 'inline-flex', gap: 4 }}>
                  {[0, 1, 2].map(i => (
                    <span key={i} style={{
                      width: 7, height: 7, borderRadius: '50%',
                      background: 'var(--vl-blue)',
                      animation: `projektChatPulse 1.4s ease-in-out ${i * 0.2}s infinite both`,
                    }} />
                  ))}
                </span>
                Durchsuche Auftragsdaten…
              </div>
            </div>
          )}

          {/* Phase: answer */}
          {phase === 'answer' && (
            <div>
              {/* Die gestellte Frage */}
              <div style={{
                fontSize: 12, fontWeight: 600, color: 'var(--text-tertiary)',
                textTransform: 'uppercase', letterSpacing: '0.05em',
                marginBottom: 6,
              }}>
                Frage
              </div>
              <div style={{
                fontSize: 14, color: 'var(--text-primary)', fontWeight: 500,
                marginBottom: 20, paddingBottom: 16,
                borderBottom: '1px solid var(--border-light)',
              }}>
                {currentQuestion}
              </div>

              {/* Antwort oder Fehler */}
              {error ? (
                <div style={{
                  padding: '12px 16px', borderRadius: 'var(--radius-md, 8px)',
                  background: '#FEF2F2', border: '1px solid #FECACA',
                  color: '#B91C1C', fontSize: 13, lineHeight: 1.5,
                }}>
                  {error}
                </div>
              ) : (
                <div style={{
                  fontSize: 14, lineHeight: 1.65, color: 'var(--text-primary)',
                  wordBreak: 'break-word',
                }}>
                  <AntwortText text={answer} />
                </div>
              )}

              {/* Neue Frage Button */}
              <div style={{ marginTop: 24, display: 'flex', gap: 8 }}>
                <button
                  onClick={resetToIdle}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 8,
                    padding: '10px 18px',
                    background: 'var(--vl-blue)', color: '#fff',
                    border: 'none', borderRadius: 'var(--radius-md, 8px)',
                    cursor: 'pointer', fontSize: 13, fontWeight: 600,
                    transition: 'opacity 0.15s',
                  }}
                  onMouseEnter={e => { e.currentTarget.style.opacity = '0.85'; }}
                  onMouseLeave={e => { e.currentTarget.style.opacity = '1'; }}
                >
                  <IconRefresh size={14} />
                  Neue Frage
                </button>
              </div>
            </div>
          )}
        </div>

        {/* Input area (sichtbar in idle + answer, nicht in loading) */}
        {phase !== 'loading' && (
          <div style={{
            display: 'flex', alignItems: 'center', gap: 8,
            padding: '12px 16px', borderTop: '1px solid var(--border-light)',
            background: 'var(--surface-white, #fff)', flexShrink: 0,
          }}>
            <input
              ref={inputRef}
              type="text"
              value={input}
              onChange={e => setInput(e.target.value)}
              onKeyDown={handleKeyDown}
              placeholder="Eigene Frage stellen…"
              style={{
                flex: 1, border: '1px solid var(--border-light)',
                borderRadius: 20, padding: '10px 16px', fontSize: 13,
                outline: 'none', background: 'var(--surface-light)',
                color: 'var(--text-primary)',
              }}
              onFocus={e => { e.currentTarget.style.borderColor = 'var(--vl-blue)'; }}
              onBlur={e => { e.currentTarget.style.borderColor = 'var(--border-light)'; }}
            />
            <button
              onClick={() => input.trim() && askQuestion(input)}
              disabled={!input.trim()}
              style={{
                width: 38, height: 38, borderRadius: '50%',
                background: input.trim() ? 'var(--vl-blue)' : 'var(--border-light)',
                border: 'none',
                cursor: input.trim() ? 'pointer' : 'default',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                transition: 'background 0.15s', flexShrink: 0,
              }}
              title="Absenden (Enter)"
            >
              <IconArrowRight size={15} stroke={input.trim() ? '#fff' : 'var(--text-tertiary)'} />
            </button>
          </div>
        )}
      </div>

      {/* Animation */}
      <style>{`
        @keyframes projektChatPulse {
          0%, 80%, 100% { opacity: 0.25; transform: scale(0.75); }
          40% { opacity: 1; transform: scale(1); }
        }
      `}</style>
    </>
  );
};

// FAB-Button: Floating Action Button unten rechts zum Öffnen des Drawers
const ProjektChatFAB = ({ onClick }) => {
  const [hovered, setHovered] = useState(false);
  return (
    <button
      onClick={onClick}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      title="Auftrag befragen"
      style={{
        position: 'fixed', bottom: 24, right: 24, zIndex: 9980,
        width: hovered ? 'auto' : 52, height: 52,
        borderRadius: hovered ? 26 : '50%',
        background: 'var(--vl-blue)', color: '#fff',
        border: 'none', cursor: 'pointer',
        boxShadow: '0 4px 16px rgba(0,0,0,0.18)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        gap: hovered ? 8 : 0,
        padding: hovered ? '0 20px 0 16px' : 0,
        transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
        overflow: 'hidden', whiteSpace: 'nowrap',
      }}
    >
      <IconHelpCircle size={20} stroke="#fff" />
      <span style={{
        fontSize: 13, fontWeight: 600,
        maxWidth: hovered ? 200 : 0,
        opacity: hovered ? 1 : 0,
        transition: 'max-width 0.25s, opacity 0.2s',
        overflow: 'hidden',
      }}>
        Auftrag befragen
      </span>
    </button>
  );
};

// ══════════════════════════════════════════════════════════════════
// 6c · SESSION SUPERSEDED OVERLAY
// Fullscreen-Overlay wenn ein anderes Gerät die Session übernommen hat.
// ══════════════════════════════════════════════════════════════════
const SessionSupersededOverlay = ({ onLogout, onReclaim }) => (
  <div style={{
    position: 'fixed', inset: 0, zIndex: 99999,
    background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)',
    display: 'flex', alignItems: 'center', justifyContent: 'center',
    padding: 24,
  }}>
    <div style={{
      background: '#fff', borderRadius: 12, padding: '32px 28px',
      maxWidth: 400, width: '100%', textAlign: 'center',
      boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
    }}>
      <div style={{
        width: 52, height: 52, borderRadius: '50%',
        background: '#FEF3C7', display: 'flex', alignItems: 'center', justifyContent: 'center',
        margin: '0 auto 16px', fontSize: 24,
      }}>
        ⚠
      </div>
      <div style={{ fontSize: 17, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 8 }}>
        Gerätelimit erreicht
      </div>
      <div style={{ fontSize: 14, color: 'var(--text-secondary)', lineHeight: 1.5, marginBottom: 24 }}>
        Ihr Account ist bereits auf zwei anderen Geräten aktiv.
        Pro Lizenz sind maximal zwei gleichzeitige Sitzungen erlaubt.
      </div>
      <button
        onClick={onReclaim}
        style={{
          width: '100%', padding: '12px 24px',
          background: 'var(--vl-blue)', color: '#fff',
          border: 'none', borderRadius: 8,
          fontSize: 14, fontWeight: 600, cursor: 'pointer',
          marginBottom: 10,
        }}
      >
        Hier fortfahren
      </button>
      <button
        onClick={onLogout}
        style={{
          width: '100%', padding: '12px 24px',
          background: 'transparent', color: 'var(--text-secondary)',
          border: '1px solid var(--border-light)', borderRadius: 8,
          fontSize: 14, fontWeight: 500, cursor: 'pointer',
        }}
      >
        Abmelden
      </button>
    </div>
  </div>
);

// ════════════════════════════════════════════════════════════════════
// ANSCHREIBEN-SYSTEM: Kontakte, Vorlagen, Briefvorlage, AnschreibenPicker
// ════════════════════════════════════════════════════════════════════

const KONTAKT_TYPES = [
  { value: 'amtsgericht', label: 'Amtsgericht' },
  { value: 'bauamt', label: 'Bauamt' },
  { value: 'gutachterausschuss', label: 'Gutachterausschuss' },
  { value: 'stadtplanung', label: 'Stadtplanung' },
  { value: 'stadtwerke_wasser', label: 'Stadtwerke (Wasser)' },
  { value: 'stadtwerke_kanal', label: 'Stadtwerke (Kanal)' },
  { value: 'tiefbauamt', label: 'Tiefbauamt' },
  { value: 'bauordnungsamt', label: 'Bauordnungsamt' },
  { value: 'bodenschutzbehoerde', label: 'Bodenschutzbehörde' },
  { value: 'denkmalschutzbehoerde', label: 'Denkmalschutzbehörde' },
  { value: 'vermessungsamt', label: 'Vermessungsamt' },
  { value: 'landratsamt', label: 'Landratsamt' },
  { value: 'finanzamt', label: 'Finanzamt' },
  { value: 'sonstiges', label: 'Sonstiges' },
];
const KONTAKT_TYPE_LABEL = Object.fromEntries(KONTAKT_TYPES.map(t => [t.value, t.label]));

const ANSCHREIBEN_TYPS_OPTIONS = [
  { value: 'grundbuchauszug', label: 'Grundbuchauszug' },
  { value: 'teilungserklaerung', label: 'Teilungserklärung' },
  { value: 'baurechtsauskunft', label: 'Baurechtsauskunft' },
  { value: 'bebauungsplan', label: 'Bebauungsplan' },
  { value: 'bodenrichtwert', label: 'Bodenrichtwert' },
  { value: 'erschliessung_strasse', label: 'Erschließung (Straße)' },
  { value: 'erschliessung_wasser', label: 'Erschließung (Wasser)' },
  { value: 'erschliessung_kanal', label: 'Erschließung (Kanal)' },
  { value: 'baulasten', label: 'Baulasten' },
  { value: 'altlasten', label: 'Altlasten' },
  { value: 'denkmalschutz', label: 'Denkmalschutz' },
  { value: 'lagekarte', label: 'Lagekarte / Luftbild' },
];

const VORLAGE_PLACEHOLDERS = [
  { id: 'OBJEKTADRESSE', desc: 'Vollständige Anschrift des Objekts' },
  { id: 'GEMARKUNG_FLST', desc: 'z.B. "Gemarkung X, Flst. Nr. Y"' },
  { id: 'GEMARKUNG', desc: 'Nur Gemarkungsname' },
  { id: 'FLURSTUECK', desc: 'Nur Flurstücknummer' },
  { id: 'AKTENZEICHEN', desc: 'Aktenzeichen des Auftrags' },
  { id: 'GERICHT', desc: 'Gerichtsname / Auftraggeber' },
  { id: 'BEZEICHNUNG', desc: 'Bewertungseinheit' },
  { id: 'SACHVERSTAENDIGER', desc: 'Name des Sachverständigen' },
  { id: 'VERFAHRENSART', desc: 'z.B. Zwangsversteigerung' },
  { id: 'SACHE', desc: 'Sache (z.B. "Müller ./. Müller")' },
];

const _formInput = {
  width: '100%', padding: '8px 10px',
  border: '1px solid var(--border-medium)', borderRadius: 'var(--radius-sm)',
  fontSize: 14, fontFamily: 'inherit', color: 'var(--text-primary)',
  background: 'var(--surface)',
};
const _formLabel = {
  display: 'block', fontSize: 11, fontWeight: 600, marginBottom: 4,
  color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.05em',
};
const _modalFooter = {
  display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-3)',
  padding: 'var(--space-4) var(--space-5)',
  borderTop: '1px solid var(--border-light)',
};


// ─── KontaktModal ───────────────────────────────────────────────────
const KontaktModal = ({ kontakt, session, workerUrl, onClose, onSaved }) => {
  const [k, setK] = useState(kontakt || { typ: 'amtsgericht' });
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  const save = async () => {
    if (!k.name || !k.typ) { setError('Name und Typ sind Pflicht.'); return; }
    setSaving(true); setError(null);
    try {
      const saved = await apiSaveKontakt(k, session, workerUrl);
      onSaved(saved);
      onClose();
    } catch (e) {
      setError(e.message);
    } finally { setSaving(false); }
  };

  return (
    <div className="modal-backdrop" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="modal" style={{ maxWidth: 640 }}>
        <div className="modal-header">
          <span>{kontakt?.id ? 'Kontakt bearbeiten' : 'Neuer Kontakt'}</span>
          <button className="modal-close" onClick={onClose}>×</button>
        </div>
        <div className="modal-body" style={{ padding: 'var(--space-5)' }}>
          {error && (
            <div style={{ padding: '8px 12px', background: 'var(--danger-bg)', color: 'var(--danger)',
                          borderRadius: 'var(--radius-sm)', fontSize: 13, marginBottom: 'var(--space-4)' }}>
              {error}
            </div>
          )}
          <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
            <div>
              <label style={_formLabel}>Name *</label>
              <input style={_formInput} value={k.name || ''} onChange={e => setK({...k, name: e.target.value})}
                     placeholder="z.B. Amtsgericht Ansbach" />
            </div>
            <div>
              <label style={_formLabel}>Typ *</label>
              <select style={_formInput} value={k.typ || ''} onChange={e => setK({...k, typ: e.target.value})}>
                {KONTAKT_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
              </select>
            </div>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
            <div>
              <label style={_formLabel}>Abteilung</label>
              <input style={_formInput} value={k.abteilung || ''} onChange={e => setK({...k, abteilung: e.target.value})}
                     placeholder="z.B. Grundbuchamt" />
            </div>
            <div>
              <label style={_formLabel}>Ansprechpartner</label>
              <input style={_formInput} value={k.ansprechpartner || ''} onChange={e => setK({...k, ansprechpartner: e.target.value})} />
            </div>
          </div>
          <div style={{ marginBottom: 'var(--space-3)' }}>
            <label style={_formLabel}>Straße & Hausnummer</label>
            <input style={_formInput} value={k.strasse || ''} onChange={e => setK({...k, strasse: e.target.value})} />
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 3fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
            <div>
              <label style={_formLabel}>PLZ</label>
              <input style={_formInput} value={k.plz || ''} onChange={e => setK({...k, plz: e.target.value})} />
            </div>
            <div>
              <label style={_formLabel}>Ort</label>
              <input style={_formInput} value={k.ort || ''} onChange={e => setK({...k, ort: e.target.value})} />
            </div>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
            <div>
              <label style={_formLabel}>E-Mail</label>
              <input style={_formInput} type="email" value={k.email || ''} onChange={e => setK({...k, email: e.target.value})} />
            </div>
            <div>
              <label style={_formLabel}>Telefon</label>
              <input style={_formInput} value={k.telefon || ''} onChange={e => setK({...k, telefon: e.target.value})} />
            </div>
            <div>
              <label style={_formLabel}>Fax</label>
              <input style={_formInput} value={k.fax || ''} onChange={e => setK({...k, fax: e.target.value})} />
            </div>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
            <div>
              <label style={_formLabel}>Gemeinde</label>
              <input style={_formInput} value={k.gemeinde || ''} onChange={e => setK({...k, gemeinde: e.target.value})}
                     placeholder="für Auto-Vorschlag" />
            </div>
            <div>
              <label style={_formLabel}>Bundesland</label>
              <input style={_formInput} value={k.bundesland || ''} onChange={e => setK({...k, bundesland: e.target.value})} />
            </div>
          </div>
          <div>
            <label style={_formLabel}>Notizen</label>
            <textarea style={{..._formInput, minHeight: 60, resize: 'vertical' }}
                      value={k.notizen || ''} onChange={e => setK({...k, notizen: e.target.value})} />
          </div>
        </div>
        <div className="modal-footer" style={_modalFooter}>
          <button className="btn btn-ghost" onClick={onClose} disabled={saving}>Abbrechen</button>
          <button className="btn btn-primary" onClick={save} disabled={saving}>
            {saving ? 'Speichern…' : 'Speichern'}
          </button>
        </div>
      </div>
    </div>
  );
};


// ─── KontakteView ───────────────────────────────────────────────────
const KontakteView = ({ session, workerUrl }) => {
  const [kontakte, setKontakte] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [search, setSearch] = useState('');
  const [filterTyp, setFilterTyp] = useState('');
  const [editKontakt, setEditKontakt] = useState(null);
  const [confirmDelete, setConfirmDelete] = useState(null);

  const load = async () => {
    setLoading(true);
    try {
      const list = await apiListKontakte(session, workerUrl);
      setKontakte(list); setError(null);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  };
  useEffect(() => { load(); }, []);

  const filtered = useMemo(() => {
    const q = search.toLowerCase();
    return kontakte.filter(k => {
      if (filterTyp && k.typ !== filterTyp) return false;
      if (!q) return true;
      return (k.name || '').toLowerCase().includes(q)
          || (k.ort || '').toLowerCase().includes(q)
          || (k.gemeinde || '').toLowerCase().includes(q);
    });
  }, [kontakte, search, filterTyp]);

  const handleDelete = async () => {
    try {
      await apiDeleteKontakt(confirmDelete.id, session, workerUrl);
      setConfirmDelete(null); load();
    } catch (e) { alert('Löschen fehlgeschlagen: ' + e.message); }
  };

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    marginBottom: 'var(--space-5)', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
        <h1 style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)', margin: 0 }}>Kontakte</h1>
        <button className="btn btn-primary" onClick={() => setEditKontakt({})}>+ Neuer Kontakt</button>
      </div>

      <div className="card" style={{ marginBottom: 'var(--space-4)' }}>
        <div style={{ display: 'flex', gap: 'var(--space-3)', flexWrap: 'wrap', alignItems: 'center' }}>
          <input type="search" placeholder="Suchen (Name, Ort, Gemeinde)…"
                 value={search} onChange={e => setSearch(e.target.value)}
                 style={{ flex: '1 1 240px', minWidth: 200, padding: '8px 12px',
                          border: '1px solid var(--border-medium)', borderRadius: 'var(--radius-sm)', fontSize: 14 }} />
          <select value={filterTyp} onChange={e => setFilterTyp(e.target.value)}
                  style={{ padding: '8px 12px', border: '1px solid var(--border-medium)',
                           borderRadius: 'var(--radius-sm)', fontSize: 14, background: 'var(--surface)' }}>
            <option value="">Alle Typen</option>
            {KONTAKT_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
          </select>
        </div>
      </div>

      {loading && <div style={{ padding: 'var(--space-8)', textAlign: 'center', color: 'var(--text-tertiary)' }}>Lade…</div>}
      {error && <div style={{ padding: 'var(--space-3)', background: 'var(--danger-bg)', color: 'var(--danger)', borderRadius: 'var(--radius-sm)' }}>{error}</div>}

      {!loading && !error && kontakte.length === 0 && (
        <div className="card" style={{ textAlign: 'center', padding: 'var(--space-8)' }}>
          <div style={{ fontSize: 14, color: 'var(--text-secondary)', marginBottom: 'var(--space-3)' }}>
            Noch keine Kontakte angelegt.
          </div>
          <div style={{ fontSize: 13, color: 'var(--text-tertiary)', marginBottom: 'var(--space-4)' }}>
            Lege Behörden und Ansprechpartner an, um sie bei Anschreiben einfach zuzuordnen.
          </div>
          <button className="btn btn-primary" onClick={() => setEditKontakt({})}>+ Ersten Kontakt anlegen</button>
        </div>
      )}

      {!loading && filtered.length > 0 && (
        <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
          {filtered.map((k, i) => (
            <div key={k.id} style={{
              display: 'grid', gridTemplateColumns: '1fr auto',
              padding: 'var(--space-4) var(--space-5)',
              borderBottom: i < filtered.length - 1 ? '1px solid var(--border-light)' : 'none',
              alignItems: 'center', gap: 'var(--space-3)',
            }}>
              <div onClick={() => setEditKontakt(k)} style={{ cursor: 'pointer', minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', marginBottom: 4, flexWrap: 'wrap' }}>
                  <span style={{ fontWeight: 600, color: 'var(--text-primary)', fontSize: 15 }}>{k.name}</span>
                  <span style={{ fontSize: 10, fontWeight: 600, padding: '2px 8px',
                                 background: 'var(--surface-blue)', color: 'var(--vl-blue)',
                                 borderRadius: 99, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
                    {KONTAKT_TYPE_LABEL[k.typ] || k.typ}
                  </span>
                </div>
                <div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
                  {[k.abteilung, k.strasse, [k.plz, k.ort].filter(Boolean).join(' ')].filter(Boolean).join(' · ')
                    || <span style={{ color: 'var(--text-tertiary)', fontStyle: 'italic' }}>Keine Adresse</span>}
                </div>
              </div>
              <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
                <button className="btn btn-ghost btn-sm" onClick={() => setEditKontakt(k)}>Bearbeiten</button>
                <button className="btn btn-ghost btn-sm" onClick={() => setConfirmDelete(k)}
                        style={{ color: 'var(--danger)' }}>Löschen</button>
              </div>
            </div>
          ))}
        </div>
      )}

      {!loading && filtered.length === 0 && kontakte.length > 0 && (
        <div className="card" style={{ textAlign: 'center', padding: 'var(--space-6)', color: 'var(--text-tertiary)' }}>
          Kein Kontakt entspricht der aktuellen Suche/Filter.
        </div>
      )}

      {editKontakt && (
        <KontaktModal kontakt={editKontakt.id ? editKontakt : null}
                      session={session} workerUrl={workerUrl}
                      onClose={() => setEditKontakt(null)} onSaved={() => load()} />
      )}

      {confirmDelete && (
        <div className="modal-backdrop" onClick={(e) => e.target === e.currentTarget && setConfirmDelete(null)}>
          <div className="modal" style={{ maxWidth: 400 }}>
            <div className="modal-header">
              <span>Kontakt löschen?</span>
              <button className="modal-close" onClick={() => setConfirmDelete(null)}>×</button>
            </div>
            <div className="modal-body" style={{ padding: 'var(--space-5)' }}>
              <strong>{confirmDelete.name}</strong> wird archiviert und nicht mehr in Auswahllisten erscheinen.
            </div>
            <div className="modal-footer" style={_modalFooter}>
              <button className="btn btn-ghost" onClick={() => setConfirmDelete(null)}>Abbrechen</button>
              <button className="btn btn-primary" onClick={handleDelete}
                      style={{ background: 'var(--danger)', borderColor: 'var(--danger)' }}>Löschen</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};


// ─── VorlageModal ───────────────────────────────────────────────────
const VorlageModal = ({ vorlage, defaultTyp, session, workerUrl, onClose, onSaved }) => {
  const [v, setV] = useState(
    vorlage || { typ: defaultTyp || '', default_kontakt_typ: null }
  );
  const [saving, setSaving] = useState(false);
  const [cloning, setCloning] = useState(false);
  const [error, setError] = useState(null);
  const bodyRef = useRef(null);
  const isSystem = !!v.ist_system_default;

  // Klonen einer System-Vorlage. Der Modal-State wechselt direkt auf den
  // Klon, der User kann nahtlos weiterbearbeiten und speichern.
  const cloneAndEdit = async () => {
    if (!v.id) return;
    setCloning(true); setError(null);
    try {
      const cloned = await apiCloneVorlage(v.id, session, workerUrl);
      setV(cloned);             // Modal zeigt jetzt den Klon → isSystem=false
      onSaved && onSaved(cloned); // Parent-Liste neu laden
    } catch (e) {
      setError(e.message);
    } finally {
      setCloning(false);
    }
  };

  const insertPlaceholder = (id) => {
    const ta = bodyRef.current;
    if (!ta) return;
    const start = ta.selectionStart || 0;
    const end = ta.selectionEnd || 0;
    const body = v.body_text || '';
    const newText = body.slice(0, start) + `{${id}}` + body.slice(end);
    setV({ ...v, body_text: newText });
    setTimeout(() => {
      ta.focus();
      ta.selectionStart = ta.selectionEnd = start + id.length + 2;
    }, 0);
  };

  const save = async () => {
    if (!v.name || !v.typ || !v.betreff || !v.body_text) {
      setError('Name, Typ, Betreff und Body sind Pflicht.'); return;
    }
    setSaving(true); setError(null);
    try {
      const saved = await apiSaveVorlage(v, session, workerUrl);
      onSaved(saved); onClose();
    } catch (e) { setError(e.message); }
    finally { setSaving(false); }
  };

  return (
    <div className="modal-backdrop" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="modal" style={{ maxWidth: 780 }}>
        <div className="modal-header">
          <span>{vorlage?.id ? 'Vorlage bearbeiten' : 'Neue Vorlage'}</span>
          <button className="modal-close" onClick={onClose}>×</button>
        </div>
        <div className="modal-body" style={{ padding: 'var(--space-5)', maxHeight: '70vh', overflowY: 'auto' }}>
          {isSystem && (
            <div style={{ padding: '10px 14px', background: 'var(--vl-orange-bg)', color: 'var(--vl-orange-dark)',
                          borderRadius: 'var(--radius-sm)', fontSize: 13, marginBottom: 'var(--space-4)' }}>
              <strong>Standard-Vorlage</strong> — diese ist schreibgeschützt. Klick auf <strong>„Klonen & bearbeiten"</strong> unten, um eine eigene Kopie zu erstellen, die du frei anpassen kannst.
            </div>
          )}
          {error && (
            <div style={{ padding: '8px 12px', background: 'var(--danger-bg)', color: 'var(--danger)',
                          borderRadius: 'var(--radius-sm)', fontSize: 13, marginBottom: 'var(--space-4)' }}>
              {error}
            </div>
          )}

          <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 'var(--space-3)', marginBottom: 'var(--space-3)' }}>
            <div>
              <label style={_formLabel}>Name *</label>
              <input style={_formInput} value={v.name || ''} onChange={e => setV({...v, name: e.target.value})}
                     disabled={isSystem} placeholder="z.B. Grundbuchauszug — Eigene" />
            </div>
            <div>
              <label style={_formLabel}>Typ *</label>
              <select
                style={{
                  ..._formInput,
                  borderColor: (!v.typ && !isSystem) ? 'var(--danger)' : 'var(--border-medium)',
                }}
                value={v.typ || ''}
                onChange={e => setV({...v, typ: e.target.value})}
                disabled={isSystem}
              >
                <option value="">— Typ wählen —</option>
                {ANSCHREIBEN_TYPS_OPTIONS.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
              </select>
            </div>
          </div>

          <div style={{ marginBottom: 'var(--space-3)' }}>
            <label style={_formLabel}>Betreff *</label>
            <input style={_formInput} value={v.betreff || ''} onChange={e => setV({...v, betreff: e.target.value})}
                   disabled={isSystem} placeholder="z.B. Antrag auf Grundbuchauszug" />
          </div>

          <div style={{ marginBottom: 'var(--space-3)' }}>
            <label style={_formLabel}>Body *</label>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 8 }}>
              {VORLAGE_PLACEHOLDERS.map(p => (
                <button key={p.id} type="button" onClick={() => insertPlaceholder(p.id)}
                        disabled={isSystem} title={p.desc}
                        style={{ padding: '4px 10px', fontSize: 11, fontFamily: 'var(--font-mono)',
                                 background: 'var(--surface-light)', color: 'var(--vl-blue)',
                                 border: '1px solid var(--border-light)', borderRadius: 'var(--radius-sm)',
                                 cursor: isSystem ? 'not-allowed' : 'pointer', opacity: isSystem ? 0.5 : 1 }}>
                  {`{${p.id}}`}
                </button>
              ))}
            </div>
            <textarea ref={bodyRef}
                      style={{ ..._formInput, minHeight: 220, resize: 'vertical', fontFamily: 'inherit' }}
                      value={v.body_text || ''} onChange={e => setV({...v, body_text: e.target.value})}
                      disabled={isSystem}
                      placeholder={'Sehr geehrte Damen und Herren,\n\nin der oben bezeichneten Sache…'} />
            <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>
              Klick auf einen Platzhalter, um ihn an der Cursor-Position einzufügen. Leere Zeilen werden zu Absätzen.
            </div>
          </div>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-3)' }}>
            <div>
              <label style={_formLabel}>Empfänger-Typ (Vorauswahl)</label>
              <select style={_formInput} value={v.default_kontakt_typ || ''}
                      onChange={e => setV({...v, default_kontakt_typ: e.target.value || null})}
                      disabled={isSystem}>
                <option value="">— kein Default —</option>
                {KONTAKT_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
              </select>
            </div>
            <div>
              <label style={_formLabel}>Anlage-Hinweis (optional)</label>
              <input style={_formInput} value={v.anlage_text || ''}
                     onChange={e => setV({...v, anlage_text: e.target.value || null})}
                     disabled={isSystem} placeholder={'z.B. "Anlage: Beschluss"'} />
            </div>
          </div>
        </div>
        <div className="modal-footer" style={_modalFooter}>
          <button className="btn btn-ghost" onClick={onClose} disabled={saving || cloning}>
            {isSystem ? 'Schließen' : 'Abbrechen'}
          </button>
          {isSystem ? (
            <button className="btn btn-primary" onClick={cloneAndEdit} disabled={cloning}>
              {cloning ? 'Klone…' : '+ Klonen & bearbeiten'}
            </button>
          ) : (
            <button className="btn btn-primary" onClick={save} disabled={saving || !v.typ}>
              {saving ? 'Speichern…' : 'Speichern'}
            </button>
          )}
        </div>
      </div>
    </div>
  );
};


// ─── BriefvorlageCard (embedded in VorlagenView) ────────────────────
const BriefvorlageCard = ({ session, workerUrl }) => {
  const [briefvorlagen, setBriefvorlagen] = useState([]);
  const [loading, setLoading] = useState(true);
  const [uploading, setUploading] = useState(false);
  const [error, setError] = useState(null);
  const fileRef = useRef(null);

  const load = async () => {
    setLoading(true);
    try {
      const list = await apiListBriefvorlagen(session, workerUrl);
      setBriefvorlagen(list); setError(null);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  };
  useEffect(() => { load(); }, []);

  const active = briefvorlagen.find(b => b.is_active);

  const handleUpload = async (file) => {
    if (!file) return;
    if (!file.name.toLowerCase().endsWith('.docx')) {
      alert('Bitte nur .docx-Dateien hochladen.'); return;
    }
    setUploading(true);
    try {
      await apiUploadBriefvorlage(file, file.name.replace(/\.docx$/i, ''), session, workerUrl);
      load();
    } catch (e) { alert('Upload fehlgeschlagen: ' + e.message); }
    finally { setUploading(false); }
  };

  return (
    <div className="card" style={{ marginBottom: 'var(--space-5)' }}>
      <div className="card-header">
        <span className="card-title">Briefvorlage</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)', flexWrap: 'wrap' }}>
        <div style={{ flex: '1 1 240px' }}>
          {loading ? (
            <span style={{ color: 'var(--text-tertiary)' }}>Lade…</span>
          ) : active ? (
            <>
              <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 2 }}>
                {active.name}
              </div>
              <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                aktiv · hochgeladen {new Date(active.created_at).toLocaleDateString('de-DE')}
              </div>
            </>
          ) : (
            <>
              <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 2 }}>
                Keine Briefvorlage hochgeladen
              </div>
              <div style={{ fontSize: 12, color: 'var(--text-tertiary)', lineHeight: 1.4 }}>
                Lade dein Briefkopf-DOCX hoch, um Anschreiben generieren zu können.
                Erwartete Jinja-Platzhalter: <code>{`{{ body }}`}</code>, <code>{`{{ empfaenger_zeile1 }}`}</code>, <code>{`{{ datum }}`}</code>, u.a.
              </div>
            </>
          )}
        </div>
        <div>
          <input ref={fileRef} type="file" accept=".docx" style={{ display: 'none' }}
                 onChange={e => { handleUpload(e.target.files?.[0]); e.target.value = ''; }} />
          <button className="btn btn-secondary" onClick={() => fileRef.current?.click()} disabled={uploading}>
            {uploading ? 'Lädt hoch…' : (active ? 'Briefvorlage ersetzen' : 'Briefvorlage hochladen')}
          </button>
        </div>
      </div>
      {error && (
        <div style={{ marginTop: 'var(--space-3)', padding: '8px 12px',
                      background: 'var(--danger-bg)', color: 'var(--danger)',
                      borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
          {error}
        </div>
      )}
    </div>
  );
};


// ─── VorlagenView ───────────────────────────────────────────────────
const VorlagenView = ({ session, workerUrl }) => {
  const [vorlagen, setVorlagen] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [editVorlage, setEditVorlage] = useState(null);
  const [confirmDelete, setConfirmDelete] = useState(null);

  const load = async () => {
    setLoading(true);
    try {
      const list = await apiListVorlagen(session, workerUrl);
      setVorlagen(list); setError(null);
    } catch (e) { setError(e.message); }
    finally { setLoading(false); }
  };
  useEffect(() => { load(); }, []);

  const typLabel = (t) => ANSCHREIBEN_TYPS_OPTIONS.find(x => x.value === t)?.label || t;

  const grouped = useMemo(() => {
    const g = {};
    const typOrder = ANSCHREIBEN_TYPS_OPTIONS.map(o => o.value);
    for (const v of vorlagen) {
      if (!g[v.typ]) g[v.typ] = [];
      g[v.typ].push(v);
    }
    // Nach typOrder sortiert ausgeben
    const sorted = {};
    for (const t of typOrder) if (g[t]) sorted[t] = g[t];
    for (const t of Object.keys(g)) if (!sorted[t]) sorted[t] = g[t];
    return sorted;
  }, [vorlagen]);

  const handleClone = async (v) => {
    try {
      const cloned = await apiCloneVorlage(v.id, session, workerUrl);
      await load();
      setEditVorlage(cloned);
    } catch (e) { alert('Klonen fehlgeschlagen: ' + e.message); }
  };
  const handleDelete = async () => {
    try {
      await apiDeleteVorlage(confirmDelete.id, session, workerUrl);
      setConfirmDelete(null); load();
    } catch (e) { alert('Löschen fehlgeschlagen: ' + e.message); }
  };

  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    marginBottom: 'var(--space-5)', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
        <h1 style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)', margin: 0 }}>Vorlagen</h1>
        <button className="btn btn-primary" onClick={() => setEditVorlage({})}>+ Neue Vorlage</button>
      </div>

      <BriefvorlageCard session={session} workerUrl={workerUrl} />

      {loading && <div style={{ padding: 'var(--space-8)', textAlign: 'center', color: 'var(--text-tertiary)' }}>Lade…</div>}
      {error && <div style={{ padding: 'var(--space-3)', background: 'var(--danger-bg)', color: 'var(--danger)', borderRadius: 'var(--radius-sm)' }}>{error}</div>}

      {!loading && !error && Object.keys(grouped).length === 0 && (
        <div className="card" style={{ textAlign: 'center', padding: 'var(--space-8)' }}>
          <div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>
            Noch keine Vorlagen angelegt.
          </div>
        </div>
      )}

      {!loading && Object.keys(grouped).map(t => (
        <div key={t} className="card" style={{ marginBottom: 'var(--space-4)', padding: 0, overflow: 'hidden' }}>
          <div style={{ padding: '8px 20px', background: 'var(--surface-raised)',
                        borderBottom: '1px solid var(--border-light)',
                        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                        gap: 'var(--space-3)' }}>
            <span style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-secondary)',
                           textTransform: 'uppercase', letterSpacing: '0.08em' }}>
              {typLabel(t)}
            </span>
            <button
              className="btn btn-ghost btn-sm"
              onClick={() => setEditVorlage({ typ: t })}
              style={{ fontSize: 11, padding: '2px 10px' }}
              title={`Neue Vorlage für ${typLabel(t)}`}
            >
              + Neu
            </button>
          </div>
          {grouped[t].map((v, i) => (
            <div key={v.id} style={{
              display: 'grid', gridTemplateColumns: '1fr auto',
              padding: 'var(--space-4) var(--space-5)',
              borderBottom: i < grouped[t].length - 1 ? '1px solid var(--border-light)' : 'none',
              alignItems: 'center', gap: 'var(--space-3)',
            }}>
              <div onClick={() => setEditVorlage(v)} style={{ cursor: 'pointer', minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', marginBottom: 4, flexWrap: 'wrap' }}>
                  <span style={{ fontWeight: 600, color: 'var(--text-primary)', fontSize: 15 }}>{v.name}</span>
                  {v.ist_system_default && (
                    <span style={{ fontSize: 10, fontWeight: 600, padding: '2px 8px',
                                   background: 'var(--vl-orange-bg)', color: 'var(--vl-orange-dark)',
                                   borderRadius: 99, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
                      Standard
                    </span>
                  )}
                </div>
                <div style={{ fontSize: 13, color: 'var(--text-secondary)',
                              overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {v.betreff}
                </div>
              </div>
              <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
                {v.ist_system_default ? (
                  <button className="btn btn-ghost btn-sm" onClick={() => handleClone(v)}>Klonen</button>
                ) : (
                  <>
                    <button className="btn btn-ghost btn-sm" onClick={() => setEditVorlage(v)}>Bearbeiten</button>
                    <button className="btn btn-ghost btn-sm" onClick={() => setConfirmDelete(v)}
                            style={{ color: 'var(--danger)' }}>Löschen</button>
                  </>
                )}
              </div>
            </div>
          ))}
        </div>
      ))}

      {editVorlage && (
        <VorlageModal vorlage={editVorlage.id ? editVorlage : null}
                      defaultTyp={editVorlage.id ? null : editVorlage.typ}
                      session={session} workerUrl={workerUrl}
                      onClose={() => setEditVorlage(null)} onSaved={() => load()} />
      )}

      {confirmDelete && (
        <div className="modal-backdrop" onClick={(e) => e.target === e.currentTarget && setConfirmDelete(null)}>
          <div className="modal" style={{ maxWidth: 400 }}>
            <div className="modal-header">
              <span>Vorlage löschen?</span>
              <button className="modal-close" onClick={() => setConfirmDelete(null)}>×</button>
            </div>
            <div className="modal-body" style={{ padding: 'var(--space-5)' }}>
              <strong>{confirmDelete.name}</strong> wird archiviert.
            </div>
            <div className="modal-footer" style={_modalFooter}>
              <button className="btn btn-ghost" onClick={() => setConfirmDelete(null)}>Abbrechen</button>
              <button className="btn btn-primary" onClick={handleDelete}
                      style={{ background: 'var(--danger)', borderColor: 'var(--danger)' }}>Löschen</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};


// ─── BulkAnschreibenModal ────────────────────────────────────────────
// Standard-Checkliste: alle anforderbaren Doc-Typen auf einen Blick,
// User wählt mehrere aus → 1 Klick erstellt alle Schreiben als ZIP.
// Anschreiben werden NICHT in der App persistiert (User-Wunsch).
const BULK_ANSCHREIBEN_DEFAULT_TYPES = new Set([
  'grundbuchauszug', 'baurechtsauskunft', 'bebauungsplan',
  'bodenrichtwert', 'altlasten', 'erschliessung_strasse',
  'erschliessung_wasser', 'erschliessung_kanal',
]);

// Mapping ANSCHREIBEN_TYPS_OPTIONS-Value → mögliche UPLOAD_TYPES-IDs für
// die Status-Anzeige "bereits hochgeladen". Mehrere Aliase möglich.
const BULK_TYP_DOK_ALIASES = {
  grundbuchauszug: ['grundbuchauszug'],
  teilungserklaerung: ['teilungserklaerung'],
  baurechtsauskunft: ['baurechtsauskunft'],
  bebauungsplan: ['bebauungsplan'],
  bodenrichtwert: ['bodenrichtwert'],
  erschliessung_strasse: ['erschliessung_strasse', 'erschliessungsnachweis'],
  erschliessung_wasser: ['erschliessung_wasser', 'erschliessungsnachweis'],
  erschliessung_kanal: ['erschliessung_kanal', 'erschliessungsnachweis'],
  baulasten: ['baulasten', 'baulastenverzeichnis'],
  altlasten: ['altlasten', 'altlastenauskunft'],
  denkmalschutz: ['denkmalschutz'],
  lagekarte: ['lagekarte'],
};

const BulkAnschreibenModal = ({
  auftragId, projektGemeinde, projektDokumente,
  session, workerUrl, onClose,
}) => {
  const [vorlagen, setVorlagen] = useState([]);
  const [allKontakte, setAllKontakte] = useState([]);
  const [items, setItems] = useState({});       // typId → { selected, vorlageId, kontaktId }
  const [loading, setLoading] = useState(true);
  const [generating, setGenerating] = useState(false);
  const [error, setError] = useState(null);
  const [progress, setProgress] = useState(null);  // { current, total, message } — informativ

  // Welche Doc-Typen sind bereits hochgeladen?
  const uploadedTypen = useMemo(() => {
    const set = new Set();
    for (const d of (projektDokumente || [])) {
      if (d.typ) set.add(d.typ);
      if (d.typ_raw) set.add(d.typ_raw);
    }
    return set;
  }, [projektDokumente]);

  const isUploaded = (typId) => {
    const aliases = BULK_TYP_DOK_ALIASES[typId] || [typId];
    return aliases.some(a => uploadedTypen.has(a));
  };

  // ─── Daten laden ───
  useEffect(() => {
    let active = true;
    (async () => {
      setLoading(true);
      try {
        const [vs, ks] = await Promise.all([
          apiListVorlagen(session, workerUrl),  // alle Vorlagen — wir filtern client-seitig pro Typ
          apiListKontakte(session, workerUrl),
        ]);
        if (!active) return;
        setVorlagen(vs);
        setAllKontakte(ks);

        // Defaults setzen: pro Typ erste passende Vorlage + passender Kontakt
        const pg = (projektGemeinde || '').toLowerCase();
        const initial = {};
        for (const opt of ANSCHREIBEN_TYPS_OPTIONS) {
          const tVorlagen = vs.filter(v => v.typ === opt.value);
          const defVorlage = tVorlagen.find(v => v.ist_system_default) || tVorlagen[0];
          const empfTyp = defVorlage?.default_kontakt_typ || null;
          const sortedKs = [...ks].sort((a, b) => {
            const aT = empfTyp && a.typ === empfTyp ? 0 : 1;
            const bT = empfTyp && b.typ === empfTyp ? 0 : 1;
            if (aT !== bT) return aT - bT;
            if (pg) {
              const aG = a.gemeinde?.toLowerCase() === pg ? 0 : 1;
              const bG = b.gemeinde?.toLowerCase() === pg ? 0 : 1;
              if (aG !== bG) return aG - bG;
            }
            return (a.name || '').localeCompare(b.name || '', 'de');
          });
          const defKontakt = sortedKs[0];
          initial[opt.value] = {
            selected: BULK_ANSCHREIBEN_DEFAULT_TYPES.has(opt.value) && !!defVorlage && !!defKontakt,
            vorlageId: defVorlage?.id || '',
            kontaktId: defKontakt?.id || '',
          };
        }
        setItems(initial);
        setError(null);
      } catch (e) {
        if (active) setError(e.message);
      } finally {
        if (active) setLoading(false);
      }
    })();
    return () => { active = false; };
  }, [session, workerUrl, projektGemeinde]);

  // Vorlagen pro Typ gruppiert
  const vorlagenByTyp = useMemo(() => {
    const map = {};
    for (const v of vorlagen) {
      if (!map[v.typ]) map[v.typ] = [];
      map[v.typ].push(v);
    }
    return map;
  }, [vorlagen]);

  // Kontakte sortiert (für Dropdown)
  const sortedKontakte = useMemo(() => {
    const pg = (projektGemeinde || '').toLowerCase();
    return [...allKontakte].sort((a, b) => {
      if (pg) {
        const aG = a.gemeinde?.toLowerCase() === pg ? 0 : 1;
        const bG = b.gemeinde?.toLowerCase() === pg ? 0 : 1;
        if (aG !== bG) return aG - bG;
      }
      return (a.name || '').localeCompare(b.name || '', 'de');
    });
  }, [allKontakte, projektGemeinde]);

  const setItem = (typId, patch) => {
    setItems(prev => ({ ...prev, [typId]: { ...prev[typId], ...patch } }));
  };

  const selectedCount = Object.values(items).filter(it => it.selected && it.vorlageId && it.kontaktId).length;
  const canGenerate = selectedCount > 0 && !generating && !loading;

  const toggleSelectAll = () => {
    const anySelected = Object.values(items).some(it => it.selected);
    const next = { ...items };
    for (const typId of Object.keys(next)) {
      next[typId] = { ...next[typId], selected: !anySelected && !!next[typId].vorlageId && !!next[typId].kontaktId };
    }
    setItems(next);
  };

  const handleGenerate = async () => {
    if (!canGenerate) return;
    const payload = Object.entries(items)
      .filter(([_, it]) => it.selected && it.vorlageId && it.kontaktId)
      .map(([_, it]) => ({ vorlage_id: it.vorlageId, kontakt_id: it.kontaktId }));
    if (payload.length === 0) return;

    setGenerating(true);
    setError(null);
    setProgress({ current: 0, total: payload.length, message: `Generiere ${payload.length} Anschreiben…` });
    try {
      const { blob, succeeded, failed } = await apiRenderAnschreibenBulk(auftragId, payload, session, workerUrl);
      // Download triggern
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = blob.type === 'application/zip'
        ? `Anschreiben_${new Date().toISOString().split('T')[0]}.zip`
        : 'Anschreiben.zip';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      setTimeout(() => URL.revokeObjectURL(url), 5000);

      if (failed > 0) {
        setProgress({ current: succeeded, total: succeeded + failed,
                      message: `${succeeded} erfolgreich, ${failed} fehlgeschlagen — siehe Fehler-Datei im ZIP.` });
      } else {
        setProgress({ current: succeeded, total: succeeded,
                      message: `${succeeded} Anschreiben heruntergeladen.` });
        // Bei vollem Erfolg: nach kurzer Verzögerung schließen
        setTimeout(() => onClose && onClose(), 1500);
      }
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setGenerating(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={(e) => e.target === e.currentTarget && !generating && onClose && onClose()}>
      <div className="modal" style={{ maxWidth: 920, width: '95%', maxHeight: '90vh', display: 'flex', flexDirection: 'column' }}>
        <div className="modal-header">
          <div>
            <div className="modal-title">Unterlagen anfordern — mehrere auf einmal</div>
            <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>
              Wähle die anzufragenden Unterlagen aus. Alle ausgewählten Schreiben werden auf einmal
              als ZIP heruntergeladen. <em>Die Schreiben werden nicht in der App gespeichert.</em>
            </div>
          </div>
          <button className="modal-close" onClick={onClose} disabled={generating}>×</button>
        </div>

        <div className="modal-body" style={{ flex: 1, overflow: 'auto', padding: 'var(--space-4) var(--space-5)' }}>
          {loading && (
            <div style={{ padding: 'var(--space-6)', textAlign: 'center', color: 'var(--text-tertiary)' }}>
              Lade Vorlagen und Kontakte…
            </div>
          )}

          {!loading && (
            <>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                            padding: '6px 0', marginBottom: 8 }}>
                <button type="button" className="btn btn-ghost btn-sm" onClick={toggleSelectAll}>
                  {Object.values(items).some(it => it.selected) ? 'Alle abwählen' : 'Alle auswählen (wo möglich)'}
                </button>
                <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
                  {selectedCount} {selectedCount === 1 ? 'Anschreiben' : 'Anschreiben'} ausgewählt
                </span>
              </div>

              {/* Tabellen-Kopf */}
              <div style={{ display: 'grid',
                            gridTemplateColumns: '32px 1.4fr 1.5fr 1.5fr 80px',
                            gap: 8, padding: '6px 8px',
                            background: 'var(--surface-raised)',
                            borderRadius: 'var(--radius-sm)', fontSize: 11,
                            fontWeight: 700, color: 'var(--text-secondary)',
                            textTransform: 'uppercase', letterSpacing: '0.04em',
                            marginBottom: 4 }}>
                <span></span>
                <span>Anfrage</span>
                <span>Vorlage</span>
                <span>Empfänger</span>
                <span style={{ textAlign: 'right' }}>Status</span>
              </div>

              {ANSCHREIBEN_TYPS_OPTIONS.map(opt => {
                const it = items[opt.value] || { selected: false, vorlageId: '', kontaktId: '' };
                const tVorlagen = vorlagenByTyp[opt.value] || [];
                const hasVorlage = tVorlagen.length > 0;
                const hasKontakt = allKontakte.length > 0;
                const uploaded = isUploaded(opt.value);
                const canSelect = hasVorlage && hasKontakt;
                return (
                  <div key={opt.value} style={{
                    display: 'grid', gridTemplateColumns: '32px 1.4fr 1.5fr 1.5fr 80px',
                    gap: 8, padding: '8px 8px', alignItems: 'center',
                    borderBottom: '1px solid var(--border-light)',
                    background: it.selected ? 'var(--success-bg, rgba(34,197,94,0.04))' : 'transparent',
                  }}>
                    <input type="checkbox"
                      checked={!!it.selected}
                      disabled={!canSelect}
                      onChange={(e) => setItem(opt.value, { selected: e.target.checked })}
                      style={{ accentColor: 'var(--vl-blue)', cursor: canSelect ? 'pointer' : 'not-allowed' }} />
                    <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)' }}>
                      {opt.label}
                    </span>
                    <select
                      value={it.vorlageId || ''}
                      onChange={(e) => setItem(opt.value, { vorlageId: e.target.value, selected: !!e.target.value && !!it.kontaktId })}
                      disabled={!hasVorlage}
                      style={{ width: '100%', padding: '4px 6px', fontSize: 12,
                               border: '1px solid var(--border-medium)',
                               borderRadius: 'var(--radius-sm)', background: 'var(--surface)' }}
                    >
                      {!hasVorlage && <option value="">— keine Vorlage —</option>}
                      {hasVorlage && <option value="">— wählen —</option>}
                      {tVorlagen.map(v => (
                        <option key={v.id} value={v.id}>
                          {v.name}{v.ist_system_default ? ' (Standard)' : ''}
                        </option>
                      ))}
                    </select>
                    <select
                      value={it.kontaktId || ''}
                      onChange={(e) => setItem(opt.value, { kontaktId: e.target.value, selected: !!e.target.value && !!it.vorlageId })}
                      disabled={!hasKontakt}
                      style={{ width: '100%', padding: '4px 6px', fontSize: 12,
                               border: '1px solid var(--border-medium)',
                               borderRadius: 'var(--radius-sm)', background: 'var(--surface)' }}
                    >
                      {!hasKontakt && <option value="">— keine Kontakte —</option>}
                      {hasKontakt && <option value="">— wählen —</option>}
                      {sortedKontakte.map(k => (
                        <option key={k.id} value={k.id}>
                          {k.name}{k.ort ? `, ${k.ort}` : ''}
                        </option>
                      ))}
                    </select>
                    <div style={{ textAlign: 'right', fontSize: 11 }}>
                      {uploaded ? (
                        <span style={{ color: 'var(--success)', fontWeight: 600 }}>✓ vorhanden</span>
                      ) : !canSelect ? (
                        <span style={{ color: 'var(--text-tertiary)' }}>
                          {!hasVorlage ? 'Vorlage fehlt' : 'Kein Kontakt'}
                        </span>
                      ) : (
                        <span style={{ color: 'var(--text-tertiary)' }}>fehlt</span>
                      )}
                    </div>
                  </div>
                );
              })}

              {error && (
                <div style={{ marginTop: 12, padding: '8px 12px',
                              background: 'var(--danger-bg)', color: 'var(--danger)',
                              borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
                  {error}
                </div>
              )}

              {progress && !error && (
                <div style={{ marginTop: 12, padding: '8px 12px',
                              background: progress.current === progress.total
                                ? 'var(--success-bg, rgba(34,197,94,0.08))'
                                : 'var(--info-bg, #EFF6FF)',
                              color: progress.current === progress.total
                                ? 'var(--success)'
                                : 'var(--vl-blue)',
                              borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
                  {progress.message}
                </div>
              )}
            </>
          )}
        </div>

        <div className="modal-footer" style={{
          display: 'flex', gap: 'var(--space-2)', justifyContent: 'flex-end',
          padding: 'var(--space-3) var(--space-5)',
          borderTop: '1px solid var(--border-light)',
        }}>
          <button className="btn btn-ghost" onClick={onClose} disabled={generating}>
            Schließen
          </button>
          <button
            className="btn btn-primary"
            onClick={handleGenerate}
            disabled={!canGenerate}
            style={{ minWidth: 240 }}
          >
            {generating
              ? `Generiere ${selectedCount}…`
              : `ZIP herunterladen (${selectedCount} ${selectedCount === 1 ? 'Schreiben' : 'Schreiben'})`}
          </button>
        </div>
      </div>
    </div>
  );
};


// ─── AnschreibenPicker (embedded in AnfrageDialog) ──────────────────
const AnschreibenPicker = ({ typId, auftragId, projektGemeinde, session, workerUrl }) => {
  const [vorlagen, setVorlagen] = useState([]);
  const [allKontakte, setAllKontakte] = useState([]);  // alle, ungefiltert
  const [vorlageId, setVorlageId] = useState('');
  const [kontaktId, setKontaktId] = useState('');
  const [loading, setLoading] = useState(true);
  const [generating, setGenerating] = useState(false);
  const [error, setError] = useState(null);

  // Einmal laden — Vorlagen für diesen Doc-Typ, alle eigenen Kontakte
  useEffect(() => {
    let active = true;
    (async () => {
      setLoading(true);
      try {
        const [vs, ks] = await Promise.all([
          apiListVorlagen(session, workerUrl, typId),
          apiListKontakte(session, workerUrl),
        ]);
        if (!active) return;
        setVorlagen(vs);
        setAllKontakte(ks);
        const def = vs.find(v => v.ist_system_default) || vs[0];
        if (def) setVorlageId(def.id);
        setError(null);
      } catch (e) {
        if (active) setError(e.message);
      } finally {
        if (active) setLoading(false);
      }
    })();
    return () => { active = false; };
  }, [typId]);

  // Aktuell gewählte Vorlage und ihr empfohlener Empfänger-Typ
  const currentVorlage = useMemo(
    () => vorlagen.find(v => v.id === vorlageId),
    [vorlagen, vorlageId]
  );
  const empfehlungsTyp = currentVorlage?.default_kontakt_typ || null;

  // Kontakte sortiert nach Relevanz, aber IMMER alle gezeigt:
  //   1. Typ matched mit Vorlage-Empfehlung
  //   2. Gemeinde matched mit Objekt-Gemeinde
  //   3. Alphabetisch
  const kontakte = useMemo(() => {
    const pg = (projektGemeinde || '').toLowerCase();
    return [...allKontakte].sort((a, b) => {
      const aTypMatch = empfehlungsTyp && a.typ === empfehlungsTyp;
      const bTypMatch = empfehlungsTyp && b.typ === empfehlungsTyp;
      if (aTypMatch && !bTypMatch) return -1;
      if (!aTypMatch && bTypMatch) return 1;
      if (pg) {
        const aGemMatch = (a.gemeinde || '').toLowerCase() === pg;
        const bGemMatch = (b.gemeinde || '').toLowerCase() === pg;
        if (aGemMatch && !bGemMatch) return -1;
        if (!aGemMatch && bGemMatch) return 1;
      }
      return (a.name || '').localeCompare(b.name || '');
    });
  }, [allKontakte, empfehlungsTyp, projektGemeinde]);

  // Auto-Select: erster nach Sortierung — aber nur, wenn noch nichts gewählt
  // oder wenn die vorherige Wahl nicht mehr in der Liste ist
  useEffect(() => {
    if (kontakte.length === 0) {
      if (kontaktId) setKontaktId('');
      return;
    }
    if (!kontaktId || !kontakte.find(k => k.id === kontaktId)) {
      setKontaktId(kontakte[0].id);
    }
  }, [kontakte]);

  const generate = async () => {
    if (!auftragId) { setError('Kein Auftrag verknüpft.'); throw new Error('no-auftrag'); }
    if (!vorlageId || !kontaktId) throw new Error('incomplete');
    setError(null);
    try {
      const blob = await apiRenderAnschreiben(auftragId, vorlageId, kontaktId, session, workerUrl);
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `Anschreiben_${typId.replace(/_/g, '-')}.docx`;
      a.click();
      URL.revokeObjectURL(url);
    } catch (e) {
      setError(e.message);
      throw e;
    }
  };

  const pickerStyle = {
    padding: 'var(--space-3) var(--space-4)', background: 'var(--surface-light)',
    borderRadius: 'var(--radius-sm)', marginBottom: 'var(--space-4)',
    border: '1px solid var(--border-light)',
  };
  const titleStyle = {
    fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)',
    marginBottom: 'var(--space-2)', textTransform: 'uppercase', letterSpacing: '0.05em',
  };
  const selectStyle = {
    width: '100%', padding: '8px 10px',
    border: '1px solid var(--border-medium)', borderRadius: 'var(--radius-sm)',
    fontSize: 13, background: 'var(--surface)', marginBottom: 'var(--space-2)',
  };

  if (loading) {
    return (
      <div style={pickerStyle}>
        <div style={titleStyle}>Anschreiben generieren</div>
        <div style={{ fontSize: 13, color: 'var(--text-tertiary)' }}>Lade Vorlagen und Kontakte…</div>
      </div>
    );
  }

  if (vorlagen.length === 0) {
    return (
      <div style={pickerStyle}>
        <div style={titleStyle}>Anschreiben generieren</div>
        <div style={{ fontSize: 13, color: 'var(--danger)' }}>
          Keine Vorlage für diesen Dokumenttyp angelegt.<br />
          → Im Tab <strong>Vorlagen</strong> oben anlegen.
        </div>
      </div>
    );
  }

  // Hinweis, falls gewählter Kontakt nicht zum Empfehlungs-Typ passt
  const selectedKontakt = kontakte.find(k => k.id === kontaktId);
  const typMismatch = empfehlungsTyp && selectedKontakt && selectedKontakt.typ !== empfehlungsTyp;
  const empfehlungLabel = empfehlungsTyp ? KONTAKT_TYPE_LABEL[empfehlungsTyp] : null;

  return (
    <div style={pickerStyle}>
      <div style={titleStyle}>Anschreiben generieren</div>
      <select style={selectStyle} value={vorlageId} onChange={e => setVorlageId(e.target.value)}>
        {vorlagen.map(v => (
          <option key={v.id} value={v.id}>
            {v.name}{v.ist_system_default ? ' (Standard)' : ''}
          </option>
        ))}
      </select>

      {allKontakte.length === 0 ? (
        <div style={{ fontSize: 13, color: 'var(--danger)', padding: '6px 0' }}>
          Du hast noch keinen Kontakt angelegt.<br />
          → Im Tab <strong>Kontakte</strong> oben anlegen.
        </div>
      ) : (
        <>
          <select style={selectStyle} value={kontaktId} onChange={e => setKontaktId(e.target.value)}>
            {kontakte.map(k => (
              <option key={k.id} value={k.id}>
                {k.name}
                {k.abteilung ? ` — ${k.abteilung}` : ''}
                {` · ${KONTAKT_TYPE_LABEL[k.typ] || k.typ}`}
                {k.ort ? ` (${k.ort})` : ''}
              </option>
            ))}
          </select>
          {typMismatch && (
            <div style={{ fontSize: 11, color: 'var(--text-tertiary)',
                          marginTop: -4, marginBottom: 'var(--space-2)' }}>
              Empfohlener Empfänger-Typ für diese Vorlage: <strong>{empfehlungLabel}</strong>.
            </div>
          )}
        </>
      )}

      {error && (
        <div style={{ fontSize: 12, color: 'var(--danger)', marginBottom: 'var(--space-2)' }}>{error}</div>
      )}

      <ActionButton
        onClick={generate}
        disabled={!vorlageId || !kontaktId}
        className="btn btn-primary action-btn"
        style={{ width: '100%', padding: '10px 16px', fontSize: 13, fontWeight: 600, justifyContent: 'center' }}
        successDuration={1300}
      >
        <IconDocument size={16} stroke="white" /> Anschreiben herunterladen
      </ActionButton>
    </div>
  );
};


// ────────────────────────────────────────────────────────────────────
// „Zuletzt angesehen" — pro Gerät in localStorage (durabel, kein Backend nötig)
// ────────────────────────────────────────────────────────────────────
const RECENT_PROJEKTE_KEY = 'vl_recent_projekte';
function _readRecents() {
  try {
    const arr = JSON.parse(localStorage.getItem(RECENT_PROJEKTE_KEY) || '[]');
    return arr.map(x => typeof x === 'string' ? { id: x } : x).filter(x => x && x.id);
  } catch { return []; }
}
function rememberRecentProjekt(id, label) {
  if (!id) return;
  try {
    let arr = _readRecents();
    const existing = arr.find(x => x.id === id);
    const lbl = label || (existing && existing.label) || null;
    arr = arr.filter(x => x.id !== id);
    arr.unshift({ id, label: lbl });
    localStorage.setItem(RECENT_PROJEKTE_KEY, JSON.stringify(arr.slice(0, 12)));
  } catch {}
}
function getRecentProjekte() { return _readRecents(); }
function getRecentProjektIds() { return _readRecents().map(x => x.id); }

// ────────────────────────────────────────────────────────────────────
// MeineAufgabenView — persönlicher Bereich: offene Aufgaben über alle
// Aufträge auf denen ich (als SV/Sachbearbeiter) eingetragen bin,
// meine Aufträge, zuletzt angesehene Aufträge.
// ────────────────────────────────────────────────────────────────────
const MeineAufgabenView = ({ session, workerUrl, userProfile, onOpen }) => {
  const [projekte, setProjekte] = useState(null);
  const [tasks, setTasks] = useState(null);
  const [tasksLoading, setTasksLoading] = useState(true);
  const [error, setError] = useState(null);
  const [onlyMine, setOnlyMine] = useState(false);
  const orgMembers = useOrgMembers(session, workerUrl);
  const memberName = (id) => (orgMembers || []).find(m => m.id === id)?.name || null;

  useEffect(() => {
    let active = true;
    ladeProjektliste()
      .then(d => { if (active) setProjekte(d); })
      .catch(e => { if (active) setError(e.message || String(e)); });
    return () => { active = false; };
  }, []);

  const myName = (userProfile?.full_name || '').trim().toLowerCase();

  const meineAuftraege = useMemo(() => {
    if (!projekte || !myName) return [];
    return projekte.filter(p => p.status !== 'archived' && (
      (p.sachverstaendiger || '').toLowerCase().includes(myName) ||
      (p.sachbearbeiter || '').toLowerCase().includes(myName)
    ));
  }, [projekte, myName]);

  // Anzeige unter „Meine Aufträge": SV/Sachbearbeiter-Aufträge plus Aufträge,
  // auf denen mir eine Aufgabe zugewiesen ist.
  const auftraegeAnzeige = useMemo(() => {
    const map = new Map();
    meineAuftraege.forEach(p => map.set(p.id, p));
    (tasks || []).forEach(t => {
      const pr = t._projekt;
      if (pr && pr.id && !map.has(pr.id) && (pr.adresse || pr.standort || pr.name)) map.set(pr.id, pr);
    });
    return Array.from(map.values());
  }, [meineAuftraege, tasks]);

  // Offene Aufgaben aus zwei Quellen zusammenführen:
  // (a) mir zugewiesen — organisationsweit über alle Aufträge
  // (b) offene Aufgaben auf Aufträgen, in denen ich SV/Sachbearbeiter bin
  useEffect(() => {
    if (!projekte) return;
    let active = true;
    setTasksLoading(true);
    (async () => {
      let token = session?.access_token;
      try { const sb = await initSupabase(); const { data } = await sb.auth.getSession(); if (data?.session?.access_token) token = data.session.access_token; } catch {}
      const byId = Object.fromEntries(projekte.map(p => [p.id, p]));
      const seen = new Map();

      // (a) mir zugewiesen, organisationsweit
      try {
        const res = await fetch(`${workerUrl}/api/meine-aufgaben`, { headers: { Authorization: `Bearer ${token}` } });
        if (res.ok) {
          const data = await res.json();
          (data.aktivitaeten || []).forEach(a => {
            if (a.erledigt) return;
            seen.set(a.id, { ...a, _projekt: byId[a.project_id] || { id: a.project_id } });
          });
        }
      } catch {}

      // (b) offene Aufgaben auf meinen Aufträgen
      const capped = meineAuftraege.slice(0, 60);
      const results = await Promise.all(capped.map(async p => {
        try {
          const res = await fetch(`${workerUrl}/api/aktivitaeten?project_id=${encodeURIComponent(p.id)}`, { headers: { Authorization: `Bearer ${token}` } });
          if (!res.ok) return [];
          const data = await res.json();
          return (data.aktivitaeten || []).filter(a => a.typ === 'aufgabe' && !a.erledigt).map(a => ({ ...a, _projekt: p }));
        } catch { return []; }
      }));
      results.flat().forEach(a => { if (!seen.has(a.id)) seen.set(a.id, a); });

      if (!active) return;
      const all = Array.from(seen.values());
      const ts = (a) => a.faellig_am ? new Date(a.faellig_am).getTime() : Infinity;
      all.sort((a, b) => ts(a) - ts(b));
      setTasks(all);
      setTasksLoading(false);
    })();
    return () => { active = false; };
  }, [projekte, meineAuftraege, session, workerUrl]);

  const recent = useMemo(() => {
    if (!projekte) return [];
    const byId = Object.fromEntries(projekte.map(p => [p.id, p]));
    return getRecentProjektIds().map(id => byId[id]).filter(Boolean).slice(0, 8);
  }, [projekte]);

  const completeTask = async (taskId) => {
    setTasks(prev => (prev || []).filter(t => t.id !== taskId));
    try { await apiPatchRow('aktivitaeten', taskId, { erledigt: true, erledigt_am: new Date().toISOString() }, session, workerUrl); } catch {}
  };

  // Abhaken in „Mein Bereich": Häkchen + sanftes Ausblenden, dann entfernen.
  const [completingIds, setCompletingIds] = useState(() => new Set());
  const handleComplete = (taskId) => {
    if (completingIds.has(taskId)) return;
    setCompletingIds(prev => new Set(prev).add(taskId));
    setTimeout(() => {
      completeTask(taskId);
      setCompletingIds(prev => { const n = new Set(prev); n.delete(taskId); return n; });
    }, 420);
  };

  const todayMid = new Date(); todayMid.setHours(0, 0, 0, 0);
  const auftragLabel = (p) => {
    const adr = (p.adresse || p.standort || '').split(',')[0];
    return [adr, p.name].filter(Boolean).join(' · ') || 'Auftrag';
  };
  const fmtDue = (d) => { try { return new Date(d).toLocaleDateString('de-DE'); } catch { return d; } };

  const SectionTitle = ({ children, count }) => (
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, margin: '0 0 var(--space-3)' }}>
      <h2 style={{ fontSize: 15, fontWeight: 700, color: 'var(--text-primary)', margin: 0 }}>{children}</h2>
      {count != null && <span style={{ fontSize: 13, color: 'var(--text-tertiary)', fontVariantNumeric: 'tabular-nums' }}>{count}</span>}
    </div>
  );

  const visibleTasks = tasks ? (onlyMine ? tasks.filter(t => t.zugewiesen_an && t.zugewiesen_an === userProfile?.id) : tasks) : null;
  const myTaskCount = tasks ? tasks.filter(t => t.zugewiesen_an && t.zugewiesen_an === userProfile?.id).length : 0;

  return (
    <div style={{ maxWidth: 1080, margin: '0 auto', padding: 'var(--space-6) var(--space-4) var(--space-10)' }}>
      <h1 style={{ fontSize: 22, fontWeight: 700, color: 'var(--text-primary)', margin: '0 0 var(--space-2)' }}>
        Mein Bereich{userProfile?.full_name ? ` · ${userProfile.full_name}` : ''}
      </h1>
      <p style={{ fontSize: 13, color: 'var(--text-tertiary)', margin: '0 0 var(--space-6)' }}>
        Deine offenen Aufgaben und Aufträge auf einen Blick — zugewiesen oder als Sachverständiger/Sachbearbeiter.
      </p>

      {error && (
        <div className="card" style={{ padding: 'var(--space-4)', color: 'var(--danger)', marginBottom: 'var(--space-5)' }}>
          Fehler beim Laden: {error}
        </div>
      )}

      {!myName && projekte && (
        <div className="card" style={{ padding: 'var(--space-5)', color: 'var(--text-secondary)', fontSize: 14, marginBottom: 'var(--space-5)' }}>
          Hinterlege deinen Namen im Profil (unten links), damit Aufträge, in denen du als Sachverständiger oder Sachbearbeiter eingetragen bist, automatisch erkannt werden.
        </div>
      )}

      {/* ── Offene Aufgaben ── */}
      <div style={{ marginBottom: 'var(--space-8)' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, margin: '0 0 var(--space-3)', flexWrap: 'wrap' }}>
          <SectionTitle count={visibleTasks ? visibleTasks.length : null}>Offene Aufgaben</SectionTitle>
          {tasks && tasks.length > 0 && (
            <Segmented ariaLabel="Aufgaben-Filter" className="seg-shrink"
              value={onlyMine ? 'mine' : 'alle'}
              onChange={(v) => setOnlyMine(v === 'mine')}
              options={[{ value: 'alle', label: `Alle (${tasks.length})` }, { value: 'mine', label: `Nur mir zugewiesen (${myTaskCount})` }]}
            />
          )}
        </div>
        {(!projekte || tasksLoading) ? (
          <div className="card" style={{ padding: 'var(--space-6)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14 }}>
            <span className="entwurf-spinner" style={{ display: 'inline-block', marginRight: 8 }} /> Aufgaben werden geladen…
          </div>
        ) : tasks.length === 0 ? (
          <div className="card" style={{ padding: 'var(--space-6)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14 }}>
            Keine offenen Aufgaben.
          </div>
        ) : visibleTasks.length === 0 ? (
          <div className="card" style={{ padding: 'var(--space-6)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14 }}>
            Dir sind aktuell keine offenen Aufgaben zugewiesen.
          </div>
        ) : (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
            {visibleTasks.map(t => {
              const overdue = t.faellig_am && new Date(t.faellig_am) < todayMid;
              const an = memberName(t.zugewiesen_an);
              return (
                <div key={t.id} className={`card lift${completingIds.has(t.id) ? ' task-removing' : ''}`} style={{ padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 12 }}>
                  <AnimatedCheckbox checked={completingIds.has(t.id)} onChange={() => handleComplete(t.id)}
                    size={20} title="Als erledigt markieren" />
                  <button onClick={() => onOpen(t._projekt.id)}
                    style={{ flex: 1, minWidth: 0, textAlign: 'left', background: 'none', border: 'none', cursor: 'pointer', padding: 0 }}>
                    <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.titel || 'Aufgabe'}</div>
                    <div style={{ fontSize: 12, color: 'var(--text-tertiary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{auftragLabel(t._projekt)}</div>
                  </button>
                  {an && (
                    <span title={`Zugewiesen an ${an}`} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 24, height: 24, borderRadius: '50%', flexShrink: 0, fontSize: 10, fontWeight: 700, background: 'var(--surface-blue)', color: 'var(--vl-blue)' }}>
                      {an.charAt(0).toUpperCase()}
                    </span>
                  )}
                  {t.faellig_am && (
                    <span className="pill" style={{ whiteSpace: 'nowrap', flexShrink: 0, fontVariantNumeric: 'tabular-nums',
                      color: overdue ? 'var(--danger)' : 'var(--text-secondary)',
                      background: overdue ? 'var(--danger-bg)' : 'var(--surface-light)' }}>
                      {overdue ? 'Überfällig · ' : 'Fällig '}{fmtDue(t.faellig_am)}
                    </span>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>

      {/* ── Meine Aufträge ── */}
      <div style={{ marginBottom: 'var(--space-8)' }}>
        <SectionTitle count={projekte ? auftraegeAnzeige.length : null}>Meine Aufträge</SectionTitle>
        {!projekte ? (
          <div className="card" style={{ padding: 'var(--space-6)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14 }}>Lädt…</div>
        ) : auftraegeAnzeige.length === 0 ? (
          <div className="card" style={{ padding: 'var(--space-6)', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14 }}>
            Du bist aktuell in keinem aktiven Auftrag als SV/Sachbearbeiter eingetragen und hast keine zugewiesenen Aufgaben.
          </div>
        ) : (
          <ProjektKarten projekte={auftraegeAnzeige} onOpen={onOpen} session={session} workerUrl={workerUrl} />
        )}
      </div>

      {/* ── Zuletzt angesehen ── */}
      {recent.length > 0 && (
        <div>
          <SectionTitle>Zuletzt angesehen</SectionTitle>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
            {recent.map(p => {
              const phase = getPhase(p);
              return (
                <button key={p.id} onClick={() => onOpen(p.id)} className="card lift"
                  style={{ padding: '8px 12px', display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer', textAlign: 'left', border: '1px solid var(--border-light)', background: 'var(--surface)' }}>
                  <ProjektImage path={p.titelbildPath} session={session} workerUrl={workerUrl} iconSize={18}
                    style={{ width: 40, height: 40, borderRadius: 'var(--radius-sm)', flexShrink: 0, border: '1px solid var(--border-light)' }} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{(p.adresse || p.standort || '').split(',')[0] || p.name || 'Auftrag'}</div>
                    <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>{p.name || ''}</div>
                  </div>
                  <span className="pill" style={{ color: phase.color, background: phase.bg, whiteSpace: 'nowrap', flexShrink: 0 }}>{phase.label}</span>
                </button>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// AppSidebar — linke Navigationsleiste (App-Hülle)
// Brand · Navigation (Mein Bereich/Aufträge/Kontakte/Vorlagen) ·
// Zuletzt angesehen · Profil-Fußzeile. Klappt zur Icon-Leiste ein,
// auf Mobil als Schublade.
// ────────────────────────────────────────────────────────────────────
const SidebarIcon = ({ name, size = 18 }) => {
  const c = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', className: 'si' };
  if (name === 'home') return (<svg {...c}><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>);
  if (name === 'orders') return (<svg {...c}><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/></svg>);
  if (name === 'contacts') return (<svg {...c}><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/></svg>);
  if (name === 'templates') return (<svg {...c}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></svg>);
  return null;
};

const AppSidebar = ({ view, onNavigate, userProfile, session, onLogout, onProfileUpdate, openProjekt, collapsed, isMobile, mobileOpen, onCloseMobile }) => {
  const [menuOpen, setMenuOpen] = useState(false);
  const [profilOpen, setProfilOpen] = useState(false);
  const menuRef = useRef(null);
  useEffect(() => {
    if (!menuOpen) return;
    const close = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); };
    document.addEventListener('mousedown', close);
    return () => document.removeEventListener('mousedown', close);
  }, [menuOpen]);

  const recents = getRecentProjekte().slice(0, 6);
  const NAV = [
    { key: 'meine_aufgaben', label: 'Mein Bereich', target: 'meine_aufgaben', icon: 'home', active: view === 'meine_aufgaben' },
    { key: 'auftraege', label: 'Aufträge', target: 'projektliste', icon: 'orders', active: view !== 'kontakte' && view !== 'vorlagen' && view !== 'meine_aufgaben' },
    { key: 'kontakte', label: 'Kontakte', target: 'kontakte', icon: 'contacts', active: view === 'kontakte' },
    { key: 'vorlagen', label: 'Vorlagen', target: 'vorlagen', icon: 'templates', active: view === 'vorlagen' },
  ];
  const go = (target) => { onNavigate(target); if (isMobile) onCloseMobile && onCloseMobile(); };
  const openRecent = (id) => { openProjekt(id); if (isMobile) onCloseMobile && onCloseMobile(); };
  const initials = (userProfile?.full_name || userProfile?.email || '?').charAt(0).toUpperCase();
  const cls = `app-sidebar${(!isMobile && collapsed) ? ' collapsed' : ''}${(isMobile && mobileOpen) ? ' mobile-open' : ''}`;

  return (
    <aside className={cls}>
      <button className="sidebar-brand" onClick={() => go('meine_aufgaben')} title="Augenschein">
        <svg width="30" height="30" viewBox="0 0 512 512" style={{ flexShrink: 0 }}>
          <g transform="translate(256,256)">
            <rect x="-110" y="-80" width="220" height="260" rx="22" fill="none" stroke="#0A2540" strokeWidth="18"/>
            <rect x="-46" y="-112" width="92" height="58" rx="16" fill="#0A2540"/>
            <rect x="-30" y="-96" width="60" height="28" rx="8" fill="var(--surface, #fff)"/>
            <g transform="translate(0, 120)">
              <path d="M0,-52 C-30,-52 -48,-30 -48,0 C-48,30 0,72 0,72 C0,72 48,30 48,0 C48,-30 30,-52 0,-52Z" fill="#EAAA3C"/>
              <circle cx="0" cy="-4" r="14" fill="var(--surface, #fff)"/>
            </g>
          </g>
        </svg>
        <span className="sidebar-label sidebar-brand-text">Augenschein</span>
      </button>

      <nav className="sidebar-nav">
        {NAV.map(n => (
          <button key={n.key} className={`sidebar-navitem${n.active ? ' active' : ''}`} onClick={() => go(n.target)} title={n.label}>
            <SidebarIcon name={n.icon} />
            <span className="sidebar-label">{n.label}</span>
          </button>
        ))}
      </nav>

      {recents.length > 0 && (
        <div className="sidebar-recents">
          <div className="sidebar-recent-label sidebar-label">Zuletzt angesehen</div>
          {recents.map(r => (
            <button key={r.id} className="sidebar-recent" onClick={() => openRecent(r.id)} title={r.label || 'Auftrag öffnen'}>
              <span className="sidebar-recent-dot" />
              <span className="sidebar-label sidebar-recent-text">{r.label || 'Auftrag öffnen'}</span>
            </button>
          ))}
        </div>
      )}

      <div className="sidebar-footer" ref={menuRef}>
        <button className="sidebar-profile" onClick={() => setMenuOpen(o => !o)} title={userProfile?.full_name || ''}>
          <span className="sidebar-avatar">{initials}</span>
          <span className="sidebar-label sidebar-profile-text">
            <span className="sidebar-profile-name">{userProfile?.full_name || 'Nutzer'}</span>
            <span className="sidebar-profile-role">{userProfile?.rolle || userProfile?.role || session?.user?.email || ''}</span>
          </span>
        </button>
        {menuOpen && (
          <div className="sidebar-menu">
            <button onClick={() => { setMenuOpen(false); setProfilOpen(true); }}>Profil bearbeiten</button>
            <button className="sidebar-menu-danger" onClick={() => { setMenuOpen(false); onLogout(); }}>Abmelden</button>
          </div>
        )}
      </div>

      {profilOpen && (
        <ProfilModal userProfile={userProfile} session={session} onClose={() => setProfilOpen(false)} onUpdate={onProfileUpdate} />
      )}
    </aside>
  );
};

const App = () => {
  const showToast = useToast();
  // Auth-State
  const [session, setSession] = useState(undefined); // undefined = loading, null = unauth, object = auth
  // ─── Stabile Session-Referenz ───
  // Problem: Supabase feuert bei Tab-Focus-Rückkehr TOKEN_REFRESHED → setSession(newObj)
  // → neues Objekt → useEffects mit session-Dependency feuern → "Lade Auftrag" → alles weg.
  // Lösung: sessionRef hält immer den aktuellen Wert, aber Effects hängen nur an session?.user?.id
  // (ändert sich nur bei echtem Login/Logout, nicht bei Token-Refresh).
  const sessionRef = useRef(session);
  sessionRef.current = session;
  const [userProfile, setUserProfile] = useState(null);
  const orgMembers = useOrgMembers(session, WORKER_URL);
  // Password-Recovery: true, sobald Supabase PASSWORD_RECOVERY-Event feuert.
  // Solange true, rendert die App statt der Projektliste den LoginView im
  // 'do-reset'-Modus, damit der Nutzer ein neues Passwort setzen kann.
  const [passwordRecoveryMode, setPasswordRecoveryMode] = useState(false);
  // Session Superseded: true wenn der Fetch-Wrapper einen 409 erhalten hat.
  const [sessionSuperseded, setSessionSuperseded] = useState(false);

  // Navigation-State
  // ── URL-Hash Persistenz: Ansicht überlebt Reload ──
  function parseNavHash() {
    const hash = window.location.hash.replace(/^#\/?/, '');
    const parts = hash.split('/');
    // App-Level-Views ohne Projekt-Kontext
    if (parts[0] === 'kontakte') {
      return { view: 'kontakte', projektId: null, gutachtenTab: 'stammdaten' };
    }
    if (parts[0] === 'vorlagen') {
      return { view: 'vorlagen', projektId: null, gutachtenTab: 'stammdaten' };
    }
    if (parts[0] === 'meine-aufgaben') {
      return { view: 'meine_aufgaben', projektId: null, gutachtenTab: 'stammdaten' };
    }
    // Format: projekt/{id}/{tab} oder projektliste
    if (parts[0] === 'projekt' && parts[1]) {
      return {
        view: parts[2] === 'auftrag' ? 'auftrag' : parts[2] ? 'gutachten' : 'dashboard',
        projektId: parts[1],
        gutachtenTab: ['stammdaten','unterlagen','objekte','ortstermin','fotos','entwurf','gutachten'].includes(parts[2]) ? parts[2] : 'stammdaten',
      };
    }
    return { view: 'meine_aufgaben', projektId: null, gutachtenTab: 'stammdaten' };
  }
  function writeNavHash(view, pId, gTab) {
    let hash = '#/projektliste';
    // App-Level-Views (kein Projekt-Kontext): immer App-View-Hash schreiben,
    // unabhängig davon ob im State noch eine alte projektId hängt
    if (view === 'projektliste') hash = '#/projektliste';
    else if (view === 'kontakte') hash = '#/kontakte';
    else if (view === 'vorlagen') hash = '#/vorlagen';
    else if (view === 'meine_aufgaben') hash = '#/meine-aufgaben';
    // Projekt-Detail-Views: pId muss gesetzt sein
    else if (pId && view === 'dashboard') hash = `#/projekt/${pId}`;
    else if (pId && view === 'auftrag') hash = `#/projekt/${pId}/auftrag`;
    else if (pId && view === 'gutachten' && gTab) hash = `#/projekt/${pId}/${gTab}`;
    else if (pId) hash = `#/projekt/${pId}`;
    if (window.location.hash !== hash) window.history.replaceState(null, '', hash);
  }

  const _initNav = parseNavHash();
  const isMobile = useIsMobile();
  const [view, setView] = useState(_initNav.view);
  const [navCollapsed, setNavCollapsed] = useState(false);   // Sidebar zur Icon-Leiste eingeklappt (Desktop)
  const [mobileNavOpen, setMobileNavOpen] = useState(false); // Sidebar-Schublade (Mobil)
  const [projektId, setProjektId] = useState(_initNav.projektId);
  const [projekt, setProjekt] = useState(null);            // Vollständiges Projekt-Objekt (nach ladeProjektDetail)
  const [projektLoading, setProjektLoading] = useState(false);
  const [projektError, setProjektError] = useState(null);
  const [aktiverGutachtenIdx, setAktiverGutachtenIdx] = useState(0);
  const [aktivesObjekt, setAktivesObjekt] = useState(0);
  const [gutachtenTab, setGutachtenTab] = useState(_initNav.gutachtenTab);
  const [uploadModalOpen, setUploadModalOpen] = useState(false);
  const [newProjektModalOpen, setNewProjektModalOpen] = useState(false);
  const [auftragReviewFiles, setAuftragReviewFiles] = useState(null); // { files: [{file, typ}], targetProjektId } | null
  const [bulkUploadOpen, setBulkUploadOpen] = useState(false);
  const [chatDrawerOpen, setChatDrawerOpen] = useState(false);

  // URL-Hash synchronisieren bei Navigation/Tab-Wechsel
  useEffect(() => {
    writeNavHash(view, projektId, gutachtenTab);
  }, [view, projektId, gutachtenTab]);

  // Pre-fill für Upload-Modal (wird vom NeuerAuftragModal-KI-Tab gesetzt)
  const [uploadPrefill, setUploadPrefill] = useState(null);  // { file, typ } | null

  // Queue für sequenzielle Mehrfach-Extraktion. Wenn der NeuerAuftragModal
  // zwei Dokumente liefert (Beschluss + Anschreiben), arbeiten wir sie
  // nacheinander durch dieselbe UploadModal-Instanz ab. Nach Apply der
  // ersten Datei rutscht die nächste automatisch rein.
  // Eintrag: { file, typ } — gleiche Form wie uploadPrefill.
  const [uploadQueue, setUploadQueue] = useState([]);
  // Gesamtanzahl der im Anlage-Flow gestarteten Dokumente. Bleibt
  // während der Abarbeitung konstant, damit position = total - queue.length
  // stabil auf "1 / 2" → "2 / 2" hochzählt.
  const [uploadTotalCount, setUploadTotalCount] = useState(0);

  // Registry-State: Tick, der nach erfolgreichem loadRegistry() hochgezählt
  // wird, um Re-Render der App zu triggern. Der eigentliche Wert liegt im
  // Modul-State _fetchedRegistry und wird via useUploadTypes() gelesen.
  const [registryTick, setRegistryTick] = useState(0);

  // Registry beim Mount laden (keine Auth erforderlich)
  useEffect(() => {
    let active = true;
    loadRegistry().then(reg => {
      if (active && reg) setRegistryTick(t => t + 1);
    });
    // Tag-Herkunft (Provenance) für Entwurf-Transparenz laden — ebenfalls auth-frei, gecacht
    loadTagProvenance().then(prov => {
      if (active && prov) setRegistryTick(t => t + 1);
    });
    return () => { active = false; };
  }, []);

  // Herkunft-Map: "tabelle:row_id:feld" → { dokument_id, titel, typ, applied_at }
  const [herkunft, setHerkunft] = useState({});

  // Auth-Check beim Mount + Auth-Listener + Session Enforcement
  useEffect(() => {
    let active = true;

    // Session-Superseded Event vom Fetch-Wrapper abfangen
    const handleSuperseded = () => {
      if (active) setSessionSuperseded(true);
    };
    window.addEventListener('session-superseded', handleSuperseded);

    (async () => {
      try {
        const sb = await initSupabase();
        const { data: { session: s } } = await sb.auth.getSession();
        if (!active) return;
        setSession(s);

        // KEIN Claim hier! onAuthStateChange('SIGNED_IN') feuert direkt
        // danach auch bei Session-Restore und übernimmt den Claim.
        // Zwei parallele Claims verursachen eine Race-Condition.

        sb.auth.onAuthStateChange((event, newSession) => {
          if (event === 'PASSWORD_RECOVERY') {
            setPasswordRecoveryMode(true);
            return;
          }

          // Session Enforcement bei SIGNED_IN:
          //   - _activeSessionId vorhanden = Session-Restore (Page-Reload,
          //     Tab-Wechsel, Token-Refresh) → bestehende ID weiterverwenden,
          //     einmal claimen damit der Server weiß welches Gerät aktiv ist.
          //   - _activeSessionId NICHT vorhanden = echter frischer Login
          //     (nach Logout oder erstem Besuch) → neue ID erzeugen + claimen.
          if (event === 'SIGNED_IN' && newSession?.access_token) {
            if (!_activeSessionId) {
              generateSessionId();
            }
            claimSession(newSession.access_token);
            setSessionSuperseded(false);
          }

          // TOKEN_REFRESHED: Re-Claim mit frischem Token, falls der initiale
          // Claim fehlgeschlagen ist (z.B. weil der Token beim Seitenstart
          // bereits abgelaufen war). Claim ist idempotent (Upsert).
          if (event === 'TOKEN_REFRESHED' && newSession?.access_token && _activeSessionId) {
            claimSession(newSession.access_token);
          }

          setSession(newSession);
          if (!newSession) {
            setProjektId(null);
            setProjekt(null);
            setView('projektliste');
            setPasswordRecoveryMode(false);
            setSessionSuperseded(false);
          }
        });
      } catch (err) {
        console.error('initSupabase failed:', err);
        if (active) setSession(null);
      }
    })();
    return () => {
      active = false;
      window.removeEventListener('session-superseded', handleSuperseded);
    };
  }, []);

  // User-Profile nach Login laden (für Organisation, Rolle, etc.)
  useEffect(() => {
    if (!session) { setUserProfile(null); return; }
    let active = true;
    (async () => {
      const sb = await initSupabase();
      const { data } = await sb
        .from('user_profiles')
        .select('*, organizations (*)')
        .eq('id', session.user.id)
        .maybeSingle();
      if (active) setUserProfile(data);
    })();
    return () => { active = false; };
  }, [session]);

  // Projekt-Detail laden, wenn sich projektId ändert
  const refreshVersionRef = useRef(0);
  const refreshProjekt = useCallback(async (showLoading = false) => {
    if (!projektId) return;
    const thisVersion = ++refreshVersionRef.current;
    if (showLoading) setProjektLoading(true);
    setProjektError(null);
    try {
      const [p, h] = await Promise.all([
        ladeProjektDetail(projektId),
        ladeHerkunft(projektId, sessionRef.current, WORKER_URL),
      ]);
      // Nur übernehmen wenn kein neuerer Refresh gestartet wurde
      if (refreshVersionRef.current !== thisVersion) return;
      if (!p) { navigate('projektliste'); return; }
      setProjekt(p);
      setHerkunft(h);
    } catch (err) {
      if (refreshVersionRef.current !== thisVersion) return;
      setProjektError(err.message || String(err));
    } finally {
      if (refreshVersionRef.current === thisVersion && showLoading) setProjektLoading(false);
    }
  }, [projektId, navigate]);

  // ─── Realtime: Live-Updates bei Multi-User ───
  const lastRealtimeRefresh = useRef(0);
  const debouncedRefresh = useCallback(() => {
    const now = Date.now();
    if (now - lastRealtimeRefresh.current < 2000) return; // Max 1 Refresh alle 2s
    lastRealtimeRefresh.current = now;
    refreshProjekt();
  }, [refreshProjekt]);

  const handleOtherUserChange = useCallback((userId, table) => {
    const member = (orgMembers || []).find(m => m.id === userId);
    const name = member?.name || 'Ein Kollege';
    showToast?.(`${name} hat Änderungen vorgenommen`, 'info', 4000);
  }, [orgMembers, showToast]);

  useRealtimeProject(projektId, session, debouncedRefresh, handleOtherUserChange);

  // Stabile Dependency: nur neu laden wenn sich der USER ändert (Login/Logout),
  // nicht bei Token-Refresh (was bei jedem Tab-Focus passiert).
  const sessionUserId = session?.user?.id;

  useEffect(() => {
    if (!projektId || !sessionUserId) { setProjekt(null); setHerkunft({}); return; }
    let active = true;
    setProjektLoading(true);
    setProjektError(null);
    Promise.all([
      ladeProjektDetail(projektId),
      ladeHerkunft(projektId, sessionRef.current, WORKER_URL),
    ])
      .then(([p, h]) => {
        if (!active) return;
        // Projekt existiert nicht (mehr) → zurück zur Liste
        if (!p) { navigate('projektliste'); setProjektLoading(false); return; }
        setProjekt(p);
        setHerkunft(h);
        setProjektLoading(false);
      })
      .catch(err => {
        if (!active) return;
        setProjektError(err.message || String(err));
        setProjektLoading(false);
      });
    return () => { active = false; };
  }, [projektId, sessionUserId]);

  // Beim View-Wechsel nach oben scrollen
  useEffect(() => {
    if (typeof window.scrollTo === 'function') window.scrollTo(0, 0);
  }, [view, projektId]);

  const navigate = useCallback((nextView, id, opts = {}) => {
    if (id !== undefined) setProjektId(id);
    // Bei Wechsel zu App-Level-Views: alten Projekt-Kontext zurücksetzen,
    // damit der URL-Hash sauber wird und Reload den richtigen View zeigt.
    else if (nextView === 'projektliste' || nextView === 'kontakte' || nextView === 'vorlagen') {
      setProjektId(null);
    }
    if (opts.gutachtenIdx !== undefined) {
      setAktiverGutachtenIdx(opts.gutachtenIdx);
      setAktivesObjekt(0);
      // Standard ist 'stammdaten'; ein expliziter opts.gutachtenTab hat Vorrang
      setGutachtenTab(opts.gutachtenTab || 'stammdaten');
    } else if (opts.gutachtenTab) {
      setGutachtenTab(opts.gutachtenTab);
    }
    setView(nextView);
    // Hash wird automatisch via useEffect synchronisiert
  }, []);

  const openProjekt = useCallback((id) => {
    rememberRecentProjekt(id);
    // Mobile: häufigster Grund einen Auftrag mobil zu öffnen ist die Ortstermin-
    // Aufnahme → direkt dorthin. Desktop: Dashboard-Übersicht wie gehabt.
    if (isMobile) {
      navigate('gutachten', id, { gutachtenIdx: 0, gutachtenTab: 'ortstermin' });
    } else {
      navigate('dashboard', id);
    }
  }, [navigate, isMobile]);
  const openAuftrag = useCallback(() => navigate('auftrag'), [navigate]);

  // ── Sidebar: Umschalten (Mobil: Schublade, Desktop: Icon-Leiste) ──
  const toggleNav = useCallback(() => {
    if (isMobile) setMobileNavOpen(o => !o);
    else setNavCollapsed(c => !c);
  }, [isMobile]);

  // Auf Detail-/Dokument-Ansichten automatisch zur Icon-Leiste einklappen,
  // auf Übersichts-Ebenen wieder ausklappen (nur Desktop, nur beim Wechsel
  // der Kategorie — eine manuelle Umschaltung bleibt sonst erhalten).
  const _detailViews = ['dashboard', 'auftrag', 'gutachten', 'dokument', 'entwurf'];
  const isDetailView = _detailViews.includes(view);
  const prevDetailRef = useRef(isDetailView);
  useEffect(() => {
    if (isMobile) return;
    if (prevDetailRef.current !== isDetailView) {
      prevDetailRef.current = isDetailView;
      setNavCollapsed(isDetailView);
    }
  }, [isDetailView, isMobile]);

  // Schublade bei View-Wechsel schließen (Mobil)
  useEffect(() => { setMobileNavOpen(false); }, [view, projektId]);

  // „Zuletzt angesehen": Label nachtragen, sobald das Projekt geladen ist.
  useEffect(() => {
    if (projekt?.id) {
      const label = (projekt.adresse || projekt.standort || '').split(',')[0] || projekt.name || null;
      rememberRecentProjekt(projekt.id, label);
    }
  }, [projekt?.id, projekt?.adresse, projekt?.standort, projekt?.name]);

  const openGutachten = useCallback((idx) => navigate('gutachten', undefined, { gutachtenIdx: idx }), [navigate]);
  const openUpload = useCallback((typOrFile) => {
    if (typOrFile instanceof File) {
      setUploadPrefill({ file: typOrFile });
    } else if (typOrFile && typeof typOrFile === 'object' && typOrFile.file instanceof File) {
      setUploadPrefill({ file: typOrFile.file, typ: typOrFile.typ, noAutoStart: typOrFile.noAutoStart });
    } else if (typeof typOrFile === 'string' && typOrFile) {
      setUploadPrefill({ typ: typOrFile });
    }
    setUploadModalOpen(true);
  }, []);
  const closeUpload = useCallback(() => setUploadModalOpen(false), []);

  const openNewProjekt = useCallback(() => setNewProjektModalOpen(true), []);
  const closeNewProjekt = useCallback(() => setNewProjektModalOpen(false), []);

  // Nach erfolgreicher Anlage: Modal schließen und ins neue Projekt navigieren.
  // Bei KI-Flow (extras.kiFiles) starten wir den UploadModal mit dem ersten
  // Dokument und queuen den Rest. Der UploadModal-Close-Handler (siehe
  // closeUploadWithReset) holt dann das nächste Queue-Element.
  const handleProjektCreated = useCallback((projektId, extras = {}) => {
    setNewProjektModalOpen(false);
    navigate('dashboard', projektId);

    // Neues Format: extras.kiFiles = [{file, typ}, ...]
    const files = Array.isArray(extras.kiFiles) ? extras.kiFiles : [];
    if (files.length === 0) return;

    // Beschluss zuerst sortieren
    const sorted = [...files].sort((a, b) => {
      if (a.typ === 'gerichtsbeschluss') return -1;
      if (b.typ === 'gerichtsbeschluss') return 1;
      return 0;
    });

    // AuftragReviewModal öffnen (statt UploadModal-Queue)
    setAuftragReviewFiles({ files: sorted, targetProjektId: projektId });
  }, [navigate]);

  // Wenn UploadModal schließt: nächstes Queue-Element starten, sonst
  // komplett zurücksetzen. Das triggert auch ein Refresh des Projekts,
  // damit die frisch applizierten Werte und Dokumente sichtbar werden.
  const closeUploadWithReset = useCallback(() => {
    setUploadModalOpen(false);

    // Queue-Peek: wenn noch ein Dokument wartet, nächstes einschieben
    setUploadQueue(prevQueue => {
      if (prevQueue.length === 0) {
        // Kein weiteres Dokument — Prefill und Total zurücksetzen
        setUploadPrefill(null);
        setUploadTotalCount(0);
        // Sicherheits-Refresh: nach dem letzten Dokument nochmal definitiv
        // laden, damit alle Beteiligte, Felder und Dokumente im Dashboard
        // sichtbar sind — auch wenn ein vorheriger Refresh durch Timing
        // noch nicht abgeschlossen war.
        setTimeout(() => refreshProjekt(), 400);
        return [];
      }
      const [next, ...rest] = prevQueue;
      setUploadPrefill({ file: next.file, typ: next.typ });
      // Refresh, damit Konflikterkennung auf aktuellem DB-Stand arbeitet,
      // DANN nächstes Dokument öffnen
      refreshProjekt().finally(() => {
        setTimeout(() => setUploadModalOpen(true), 200);
      });
      return rest;
    });
  }, [refreshProjekt]);

  // Dokument in neuem Tab öffnen (Signed URL vom Worker)
  const openDocument = useCallback((dokumentId) => {
    oeffneDokument(dokumentId, session, WORKER_URL);
  }, [session]);

  const switchGutachten = useCallback((idx) => {
    setAktiverGutachtenIdx(idx);
    setAktivesObjekt(0);
    setGutachtenTab('stammdaten');
  }, []);

  const handleReclaim = useCallback(async () => {
    if (!session?.access_token) return;
    // Neue Session-ID generieren und claimen (verdrängt die älteste Session)
    generateSessionId();
    await claimSession(session.access_token);
    setSessionSuperseded(false);
  }, [session]);

  const handleLogout = useCallback(async () => {
    // Session-ID löschen (Session Enforcement)
    clearSessionId();
    // IndexedDB löschen: Keine Gerichtsdaten auf dem Gerät hinterlassen
    try {
      const dbs = await indexedDB.databases();
      for (const db of dbs) {
        if (db.name) indexedDB.deleteDatabase(db.name);
      }
    } catch (e) { console.warn('[Logout] IDB cleanup:', e); }
    setSessionSuperseded(false);
    const sb = await initSupabase();
    await sb.auth.signOut();
  }, []);

  // Session-Timeout: Auto-Logout nach 8h Inaktivität
  const { showWarning: sessionWarning, extendSession } = useSessionTimeout(session, handleLogout);

  // ─── Escape schließt das oberste offene Overlay ───
  // Zusätzlicher, erwarteter Tastatur-Schließweg. Die bestehenden Schließwege
  // (Klick auf Hintergrund / Schließen-Button) bleiben unverändert; pro
  // Tastendruck wird nur EIN Overlay geschlossen (oberstes zuerst).
  useEffect(() => {
    const onEscape = (e) => {
      if (e.key !== 'Escape' || e.defaultPrevented) return;
      // In Textfeldern hat Escape oft eigene Bedeutung (z. B. Inline-Editor
      // abbrechen) — dort das Overlay NICHT schließen.
      const t = e.target;
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable)) return;
      if (uploadModalOpen) closeUploadWithReset();
      else if (newProjektModalOpen) closeNewProjekt();
      else if (auftragReviewFiles) setAuftragReviewFiles(null);
      else if (bulkUploadOpen) setBulkUploadOpen(false);
      else if (chatDrawerOpen) setChatDrawerOpen(false);
    };
    window.addEventListener('keydown', onEscape);
    return () => window.removeEventListener('keydown', onEscape);
  }, [uploadModalOpen, newProjektModalOpen, auftragReviewFiles, bulkUploadOpen, chatDrawerOpen, closeUploadWithReset, closeNewProjekt]);

  // Objekt-Kontext für Upload-Dialog.
  // Immer verfügbar wenn ein Projekt geladen ist. Bei view='gutachten' die Objekte
  // des aktiven Gutachtens, sonst die des ersten Gutachtens (Default fürs Dashboard).
  const kontextGutachten = projekt?.gutachten?.[
    view === 'gutachten' ? aktiverGutachtenIdx : 0
  ];
  const objektContext = kontextGutachten?.objekte || [];

  // ─── Rendering-Gates ───
  if (session === undefined) {
    return (
      <div style={{ padding: 80, textAlign: 'center', color: 'var(--text-tertiary)' }}>
        Lade…
      </div>
    );
  }

  // Recovery-Flow geht vor: sobald PASSWORD_RECOVERY-Event kam, zeigen wir
  // das Reset-Formular — unabhängig davon, ob Supabase im Hintergrund
  // bereits eine Session gesetzt hat. Sonst würde der User kurz die
  // Projektliste sehen und der Reset-Screen wäre weg.
  if (passwordRecoveryMode) {
    return <LoginView
      initialMode="do-reset"
      onLoginSuccess={() => {
        // Nach erfolgreichem Passwort-Update: Recovery-Mode verlassen,
        // den Auth-Listener die echte Session übernehmen lassen.
        setPasswordRecoveryMode(false);
      }}
    />;
  }

  if (session === null) {
    return <LoginView onLoginSuccess={() => { /* Auth-Listener übernimmt */ }} />;
  }

  // Dashboard-Daten sind nur verfügbar, wenn Projekt geladen ist
  const projektBereit = view === 'projektliste' || (projekt && !projektLoading);

  return (
    <>
      {/* Session Superseded Overlay */}
      {sessionSuperseded && (
        <SessionSupersededOverlay onLogout={handleLogout} onReclaim={handleReclaim} />
      )}

      {/* Session-Timeout Warnung */}
      {sessionWarning && (
        <div style={{
          position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
          background: '#B45309', color: 'white', padding: '10px 16px',
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          fontSize: 13, fontWeight: 500,
        }}>
          <span>Deine Sitzung läuft in 5 Minuten ab. Zum Schutz deiner Daten wirst du automatisch abgemeldet.</span>
          <button
            onClick={extendSession}
            style={{
              background: 'white', color: '#B45309', border: 'none',
              padding: '4px 12px', borderRadius: 6, fontWeight: 600,
              cursor: 'pointer', flexShrink: 0, marginLeft: 12,
            }}
          >
            Sitzung verlängern
          </button>
        </div>
      )}

      <div className="app-shell">
        <AppSidebar
          view={view}
          onNavigate={navigate}
          userProfile={userProfile}
          session={session}
          onLogout={handleLogout}
          onProfileUpdate={(updated) => setUserProfile(updated)}
          openProjekt={openProjekt}
          collapsed={navCollapsed}
          isMobile={isMobile}
          mobileOpen={mobileNavOpen}
          onCloseMobile={() => setMobileNavOpen(false)}
        />
        {isMobile && mobileNavOpen && (
          <div className="sidebar-backdrop" onClick={() => setMobileNavOpen(false)} />
        )}
        <div className="app-main">
          <TopBar
            view={view}
            projekt={projekt}
            aktiverGutachtenIdx={aktiverGutachtenIdx}
            gutachtenCount={projekt?.gutachten?.length || 0}
            onNavigate={navigate}
            onToggleNav={toggleNav}
          />

          <div className="app-container">
        {/* Projekt-Header + Tab-Navigation (nur wenn ein Projekt offen ist) */}
        {view !== 'projektliste' && view !== 'kontakte' && view !== 'vorlagen' && projekt && !projektLoading && (
          <ProjektStickyHeader
            p={projekt}
            view={view}
            gutachtenTab={gutachtenTab}
            navigate={navigate}
            setGutachtenTab={setGutachtenTab}
            orgMembers={orgMembers}
            session={session}
            workerUrl={WORKER_URL}
            onRefresh={refreshProjekt}
          />
        )}
        {projektError && (
          <PrototypeHint>
            <strong>Fehler beim Laden:</strong> {projektError}
          </PrototypeHint>
        )}

        <div key={`${view}:${projekt && !projektLoading ? 'r' : 'l'}`} className="view-fade">
        {view === 'projektliste' && (
          <ProjektlisteView
            onOpen={openProjekt}
            onNewProjekt={openNewProjekt}
            session={session}
            workerUrl={WORKER_URL}
            userProfile={userProfile}
          />
        )}

        {view === 'kontakte' && session && (
          <KontakteView session={session} workerUrl={WORKER_URL} />
        )}

        {view === 'vorlagen' && session && (
          <VorlagenView session={session} workerUrl={WORKER_URL} />
        )}

        {view === 'meine_aufgaben' && session && (
          <MeineAufgabenView session={session} workerUrl={WORKER_URL} userProfile={userProfile} onOpen={openProjekt} />
        )}

        {view !== 'projektliste' && view !== 'kontakte' && view !== 'vorlagen' && view !== 'meine_aufgaben' && projektLoading && (
          <div style={{ padding: 'var(--space-12)', textAlign: 'center', color: 'var(--text-tertiary)' }}>
            Lade Auftrag…
          </div>
        )}

        {view === 'dashboard' && projekt && !projektLoading && (
          <DashboardView
            p={projekt}
            onOpenAuftrag={openAuftrag}
            onOpenGutachten={openGutachten}
            onOpenUpload={openUpload}
            session={session}
            workerUrl={WORKER_URL}
            onRefresh={refreshProjekt}
            orgMembers={orgMembers}
          />
        )}
        {view === 'auftrag' && projekt && !projektLoading && (
          <AuftragView
            p={projekt}
            herkunft={herkunft}
            onOpenDocument={openDocument}
            session={session}
            workerUrl={WORKER_URL}
            onRefresh={refreshProjekt}
            orgMembers={orgMembers}
          />
        )}
        {view === 'gutachten' && projekt && !projektLoading && (
          <GutachtenView
            p={projekt}
            aktiverGutachtenIdx={aktiverGutachtenIdx}
            aktivesObjekt={aktivesObjekt}
            gutachtenTab={gutachtenTab}
            onSwitchGutachten={switchGutachten}
            onSwitchObjekt={setAktivesObjekt}
            onSwitchTab={setGutachtenTab}
            onOpenAuftrag={openAuftrag}
            onOpenUpload={openUpload}
            onOpenBulkUpload={() => setBulkUploadOpen(true)}
            onDeleteGutachten={async (gutachtenId) => {
              try {
                const result = await apiDeleteGutachten(gutachtenId, session, WORKER_URL);
                if (result.projectDeleted) {
                  // Letztes Gutachten → Projekt auch gelöscht → zurück zur Liste
                  setProjektId(null);
                  setProjekt(null);
                } else {
                  // Noch Gutachten übrig → Projekt neu laden
                  refreshProjekt();
                }
              } catch (e) {
                console.error('Delete failed:', e);
                alert('Löschen fehlgeschlagen: ' + e.message);
              }
            }}
            herkunft={herkunft}
            onOpenDocument={openDocument}
            session={session}
            workerUrl={WORKER_URL}
            userProfile={userProfile}
            onRefresh={refreshProjekt}
          />
        )}
        </div>
      </div>
        </div>
      </div>

      {uploadModalOpen && projekt && (() => {
        // Queue-Anzeige nur bei Multi-Doc-Flow. Solange uploadTotalCount>1
        // aktiv ist, rechnen wir die aktuelle Position dynamisch aus:
        // position = total - (Anzahl der noch in der Queue wartenden Docs).
        // Bei 2 Docs: Start position=1 (queue.length=1), nach apply
        // position=2 (queue.length=0).
        const total = uploadTotalCount;
        const remaining = uploadQueue.length;
        const ctx = total > 1 ? {
          position: total - remaining,
          total,
          typLabel: dokumenttypLabel(uploadPrefill?.typ),
          nextTypLabel: uploadQueue[0]
            ? dokumenttypLabel(uploadQueue[0].typ)
            : null,
        } : null;
        return (
          <UploadModal
            onClose={closeUploadWithReset}
            onApplied={refreshProjekt}
            objektContext={objektContext}
            projektId={projekt.id}
            aktiverGutachtenId={kontextGutachten?.id}
            session={session}
            workerUrl={WORKER_URL}
            prefillFile={uploadPrefill?.file}
            prefillTyp={uploadPrefill?.typ}
            noAutoStart={uploadPrefill?.noAutoStart}
            queueContext={ctx}
            projektName={projekt?.name}
            autoApply={uploadTotalCount > 1 && ctx?.position > 1}
            onRenameProjekt={async (newName) => {
              await apiPatchRow('projects', projekt.id, { name: newName }, session, WORKER_URL);
              await refreshProjekt();
            }}
          />
        );
      })()}

      {newProjektModalOpen && (
        <NeuerAuftragModal
          onClose={closeNewProjekt}
          onCreated={handleProjektCreated}
          session={session}
          workerUrl={WORKER_URL}
          userProfile={userProfile}
          orgMembers={orgMembers}
        />
      )}

      {auftragReviewFiles && (
        <AuftragReviewModal
          files={auftragReviewFiles.files}
          projektId={auftragReviewFiles.targetProjektId}
          gutachtenId={projekt && projekt.id === auftragReviewFiles.targetProjektId ? (projekt.gutachten || [])[0]?.id : null}
          session={session}
          workerUrl={WORKER_URL}
          projektName={projekt && projekt.id === auftragReviewFiles.targetProjektId ? projekt.name : ''}
          onRenameProjekt={async (newName) => {
            await apiPatchRow('projects', auftragReviewFiles.targetProjektId, { name: newName }, session, WORKER_URL);
          }}
          onClose={() => setAuftragReviewFiles(null)}
          onComplete={refreshProjekt}
        />
      )}

      {/* Bulk-Upload-Modal (App-Level, überlebt Desktop-Wechsel + Tab-Re-Renders) */}
      {bulkUploadOpen && projekt && (
        <BulkUploadModal
          projektId={projekt.id}
          gutachtenId={(projekt.gutachten || [])[aktiverGutachtenIdx]?.id || null}
          allObjekte={(projekt.gutachten || []).flatMap(gt => (gt.objekte || []).map(o => ({ ...o, gutachten_id: gt.id })))}
          session={session}
          workerUrl={WORKER_URL}
          onClose={() => setBulkUploadOpen(false)}
          onComplete={() => { setBulkUploadOpen(false); refreshProjekt(); }}
        />
      )}

      {/* Auftrag befragen: FAB + Drawer */}
      {projekt && !projektLoading && view !== 'projektliste' && (
        <>
          {!chatDrawerOpen && (
            <ProjektChatFAB onClick={() => setChatDrawerOpen(true)} />
          )}
          <ProjektChatDrawer
            open={chatDrawerOpen}
            onClose={() => setChatDrawerOpen(false)}
            projektId={projekt.id}
            projektName={projekt.name}
            session={session}
            workerUrl={WORKER_URL}
          />
        </>
      )}

      <div style={{
        textAlign: 'center', padding: '24px 16px 16px',
        fontSize: 11, color: 'var(--text-tertiary)', opacity: 0.6,
        display: 'flex', justifyContent: 'center', alignItems: 'center',
        gap: 8, flexWrap: 'wrap',
      }}>
        <span>powered by <strong>The Tackle Company</strong> · © 2026</span>
        <span>·</span>
        <a href="https://augenschein.app/datenschutz" target="_blank" rel="noopener" style={{ color: 'inherit' }}>Datenschutz</a>
        <span>·</span>
        <a href="https://augenschein.app/impressum" target="_blank" rel="noopener" style={{ color: 'inherit' }}>Impressum</a>
      </div>
    </>
  );
};

// ══════════════════════════════════════════════════════════════════
// 7 · MOUNT
// ══════════════════════════════════════════════════════════════════
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<ToastProvider><App /></ToastProvider>);
