// Quick City — Shared utilities (animated counter, scroll reveal, map, before/after)

/* ─────────────────────────────────────────
   useInView — fire once when element enters viewport
───────────────────────────────────────── */
function useInView(ref, options = {}) {
  const [inView, setInView] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current) return;
    const obs = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) { setInView(true); obs.disconnect(); }
    }, { threshold: 0.2, ...options });
    obs.observe(ref.current);
    return () => obs.disconnect();
  }, []);
  return inView;
}

/* ─────────────────────────────────────────
   AnimatedNumber — counts up from 0 to target when scrolled into view
   Props: value (string like "150+", "24/7", "8.400", "47", "100%"), duration (ms)
───────────────────────────────────────── */
function AnimatedNumber({ value, duration = 1600, className, style }) {
  const ref = React.useRef(null);
  const inView = useInView(ref);
  const [display, setDisplay] = React.useState(value);

  // Parse numeric part + prefix/suffix
  const parsed = React.useMemo(() => {
    const str = String(value);
    // Match leading non-numeric, the number (with dots/commas as thousand sep), trailing
    const m = str.match(/^([^\d-]*)([\d.,]+)(.*)$/);
    if (!m) return { animate: false, str };
    const prefix = m[1] || '';
    const numStr = m[2];
    const suffix = m[3] || '';
    // Detect if it uses thousand-separators (dots/commas)
    const usesDotSep = /\d\.\d{3}(\D|$)/.test(numStr);
    const usesCommaSep = /\d,\d{3}(\D|$)/.test(numStr);
    const cleanedForParse = usesDotSep
      ? numStr.replace(/\./g, '')
      : usesCommaSep
        ? numStr.replace(/,/g, '')
        : numStr.replace(',', '.');
    const num = parseFloat(cleanedForParse);
    if (isNaN(num)) return { animate: false, str };
    return { animate: true, num, prefix, suffix, usesDotSep, usesCommaSep, original: str };
  }, [value]);

  React.useEffect(() => {
    if (!inView || !parsed.animate) { setDisplay(value); return; }
    let raf;
    const start = performance.now();
    const target = parsed.num;
    const isInteger = Number.isInteger(target);
    const tick = (now) => {
      const t = Math.min(1, (now - start) / duration);
      // ease-out cubic
      const eased = 1 - Math.pow(1 - t, 3);
      const current = target * eased;
      let numFormatted;
      if (isInteger) {
        numFormatted = Math.round(current).toString();
        if (parsed.usesDotSep) {
          numFormatted = numFormatted.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
        } else if (parsed.usesCommaSep) {
          numFormatted = numFormatted.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
        }
      } else {
        numFormatted = current.toFixed(1).replace('.', parsed.usesCommaSep ? ',' : '.');
      }
      setDisplay(parsed.prefix + numFormatted + parsed.suffix);
      if (t < 1) raf = requestAnimationFrame(tick);
      else setDisplay(parsed.original);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [inView, value]);

  return <span ref={ref} className={className} style={style}>{display}</span>;
}

/* ─────────────────────────────────────────
   Reveal — wraps children; adds .is-visible class when scrolled into view
   Use with .reveal CSS class for opacity/translate transition.
───────────────────────────────────────── */
function Reveal({ children, delay = 0, as: Tag = 'div', className = '', ...rest }) {
  const ref = React.useRef(null);
  const inView = useInView(ref);
  return (
    <Tag
      ref={ref}
      className={'reveal ' + (inView ? 'is-visible ' : '') + className}
      style={{ transitionDelay: `${delay}ms`, ...(rest.style || {}) }}
      {...rest}
    >
      {children}
    </Tag>
  );
}

/* ─────────────────────────────────────────
   BeforeAfter — slider revealing two images
   Props: beforeSrc, afterSrc, beforeLabel?, afterLabel?
───────────────────────────────────────── */
function BeforeAfter({ beforeSrc, afterSrc, beforeLabel = 'Vorher', afterLabel = 'Nachher', alt = '' }) {
  const containerRef = React.useRef(null);
  const beforeWrapRef = React.useRef(null);
  const afterWrapRef = React.useRef(null);
  const handleRef = React.useRef(null);
  const draggingRef = React.useRef(false);
  const posRef = React.useRef(50);
  const rafRef = React.useRef(null);
  const [bothLoaded, setBothLoaded] = React.useState(false);
  const loadedCountRef = React.useRef(0);

  // Preload both images before rendering — avoids brief flash of after-only
  React.useEffect(() => {
    setBothLoaded(false);
    loadedCountRef.current = 0;
    const onOne = () => {
      loadedCountRef.current += 1;
      if (loadedCountRef.current >= 2) setBothLoaded(true);
    };
    const i1 = new Image();
    const i2 = new Image();
    i1.onload = onOne; i1.onerror = onOne; i1.src = beforeSrc;
    i2.onload = onOne; i2.onerror = onOne; i2.src = afterSrc;
    // Fallback after 1.5s in case load events don't fire
    const t = setTimeout(() => setBothLoaded(true), 1500);
    return () => { clearTimeout(t); i1.onload = null; i1.onerror = null; i2.onload = null; i2.onerror = null; };
  }, [beforeSrc, afterSrc]);

  const updatePos = React.useCallback((pct) => {
    posRef.current = pct;
    if (rafRef.current) return;
    rafRef.current = requestAnimationFrame(() => {
      rafRef.current = null;
      if (beforeWrapRef.current) {
        beforeWrapRef.current.style.clipPath = `inset(0 ${100 - posRef.current}% 0 0)`;
      }
      if (afterWrapRef.current) {
        afterWrapRef.current.style.clipPath = `inset(0 0 0 ${posRef.current}%)`;
      }
      if (handleRef.current) {
        handleRef.current.style.left = posRef.current + '%';
      }
    });
  }, []);

  const updateFromEvent = React.useCallback((clientX) => {
    if (!containerRef.current) return;
    const rect = containerRef.current.getBoundingClientRect();
    const x = clientX - rect.left;
    const pct = Math.max(0, Math.min(100, (x / rect.width) * 100));
    updatePos(pct);
  }, [updatePos]);

  React.useEffect(() => {
    const onMove = (e) => {
      if (!draggingRef.current) return;
      const clientX = e.touches ? e.touches[0].clientX : e.clientX;
      updateFromEvent(clientX);
      if (e.touches) e.preventDefault();
    };
    const onUp = () => { draggingRef.current = false; };
    window.addEventListener('mousemove', onMove, { passive: true });
    window.addEventListener('touchmove', onMove, { passive: false });
    window.addEventListener('mouseup', onUp);
    window.addEventListener('touchend', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('touchmove', onMove);
      window.removeEventListener('mouseup', onUp);
      window.removeEventListener('touchend', onUp);
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
    };
  }, [updateFromEvent]);

  return (
    <div
      ref={containerRef}
      className={'ba' + (bothLoaded ? ' is-ready' : ' is-loading')}
      onMouseDown={(e) => { if (!bothLoaded) return; draggingRef.current = true; updateFromEvent(e.clientX); }}
      onTouchStart={(e) => { if (!bothLoaded) return; draggingRef.current = true; updateFromEvent(e.touches[0].clientX); }}
    >
      {!bothLoaded && <div className="ba__loader" aria-label="Bild lädt"><div className="ba__loader-spinner"></div></div>}
      <img className="ba__img ba__img--after" src={afterSrc} alt={alt} loading="eager" decoding="async" />
      <div ref={beforeWrapRef} className="ba__before-wrap" style={{ clipPath: 'inset(0 50% 0 0)', background: '#0a0a0a' }}>
        <img className="ba__img ba__img--before" src={beforeSrc} alt={alt} loading="eager" decoding="async" />
        <div className="ba__label ba__label--before">{beforeLabel}</div>
      </div>
      <div ref={afterWrapRef} className="ba__after-wrap" style={{ clipPath: 'inset(0 0 0 50%)' }}>
        <div className="ba__label ba__label--after">{afterLabel}</div>
      </div>
      <div ref={handleRef} className="ba__handle" style={{ left: '50%' }}>
        <div className="ba__handle-line"></div>
        <div className="ba__handle-knob">
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M5 4 L1 8 L5 12 M11 4 L15 8 L11 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
        </div>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────
   GermanyMap — schematic SVG map with pins
   Props: pins = [{ x, y, label, highlight }], viewBox optional
───────────────────────────────────────── */
/* ─────────────────────────────────────────
   GermanyMap — D3-geo with Bundesländer outlines + country border
───────────────────────────────────────── */
const GERMANY_VIEWBOX = { w: 100, h: 130 };
let _germanyDataCache = null;

async function loadGermanyData() {
  if (_germanyDataCache) return _germanyDataCache;

  const out = { country: null, states: null };

  // 1. Country outline (from world-atlas)
  try {
    const resp = await fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json');
    const topo = await resp.json();
    const all = window.topojson.feature(topo, topo.objects.countries);
    out.country = all.features.find(f => f.id === '276'); // ISO 276 = Germany
  } catch (e) {
    console.warn('country geo load failed', e);
  }

  // 2. Bundesländer (isellsoap/deutschlandGeoJSON via jsdelivr)
  try {
    const resp = await fetch('https://cdn.jsdelivr.net/gh/isellsoap/deutschlandGeoJSON@main/2_bundeslaender/3_mittel.geo.json');
    const states = await resp.json();
    out.states = states;
  } catch (e) {
    console.warn('Bundesländer geo load failed', e);
  }

  // Fallback if everything fails
  if (!out.country && !out.states) out.country = window.QC_GERMANY_GEOJSON;

  _germanyDataCache = out;
  return out;
}

function GermanyMap({ pins = [], hubIdx = null, title, lead }) {
  const [data, setData] = React.useState(_germanyDataCache);

  React.useEffect(() => {
    if (data) return;
    let cancelled = false;
    loadGermanyData().then(d => { if (!cancelled) setData(d); });
    return () => { cancelled = true; };
  }, []);

  const projected = React.useMemo(() => {
    if (typeof window === 'undefined' || !window.d3 || !data) return null;
    const refGeo = data.states || data.country;
    if (!refGeo) return null;
    const w = GERMANY_VIEWBOX.w, h = GERMANY_VIEWBOX.h;
    const pad = 4;
    const proj = window.d3.geoMercator().fitExtent(
      [[pad, pad], [w - pad, h - pad]],
      refGeo
    );
    const path = window.d3.geoPath(proj);
    const countryPath = data.country ? path(data.country) : null;
    const statePaths = data.states && data.states.features
      ? data.states.features.map(f => path(f))
      : null;
    const points = pins.map(p => {
      if (typeof p.lat === 'number' && typeof p.lng === 'number') {
        const r = proj([p.lng, p.lat]) || [0, 0];
        return { x: r[0], y: r[1], label: p.label, labelPos: p.labelPos };
      }
      return p;
    });
    return { countryPath, statePaths, points };
  }, [pins, data]);

  return (
    <div className="qc-map">
      {title && <div className="qc-map__head">
        <h3 className="qc-map__title">{title}</h3>
        {lead && <p className="qc-map__lead">{lead}</p>}
      </div>}
      <div className="qc-map__inner">
        <svg className="qc-map__svg" viewBox={`0 0 ${GERMANY_VIEWBOX.w} ${GERMANY_VIEWBOX.h}`} xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
          {projected ? (
            <>
              {/* Bundesländer fills + subtle internal borders */}
              {projected.statePaths && projected.statePaths.map((d, i) => (
                <path key={'st-'+i} d={d} fill="rgba(255,255,255,0.04)" stroke="rgba(255,255,255,0.18)" strokeWidth="0.22" strokeLinejoin="round" />
              ))}
              {/* Country outline overlay (stronger border) */}
              {projected.countryPath && (
                <path d={projected.countryPath} fill="none" stroke="rgba(255,255,255,0.55)" strokeWidth="0.45" strokeLinejoin="round" />
              )}
              {/* Hub pulse */}
              {projected.points.map((p, i) => i === hubIdx && (
                <circle key={'pulse-'+i} cx={p.x} cy={p.y} r="2.5" fill="var(--accent)" opacity="0.4">
                  <animate attributeName="r" from="2.5" to="9" dur="2s" repeatCount="indefinite" />
                  <animate attributeName="opacity" from="0.4" to="0" dur="2s" repeatCount="indefinite" />
                </circle>
              ))}
              {/* Connection lines from hub */}
              {hubIdx != null && projected.points[hubIdx] && projected.points.map((p, i) => i === hubIdx ? null : (
                <line key={'ln-'+i} x1={projected.points[hubIdx].x} y1={projected.points[hubIdx].y} x2={p.x} y2={p.y} stroke="var(--accent)" strokeWidth="0.28" opacity="0.5" strokeDasharray="0.7 1.2" />
              ))}
              {/* Pins */}
              {projected.points.map((p, i) => {
                // labelPos override: 'top' | 'bottom' | 'left' | 'right'; otherwise auto (right, flips left near edge)
                const auto = p.x > GERMANY_VIEWBOX.w * 0.7 ? 'left' : 'right';
                const pos = p.labelPos || auto;
                let tx, ty, anchor;
                if (pos === 'left')      { tx = p.x - 2.6; ty = p.y + 1;   anchor = 'end'; }
                else if (pos === 'right'){ tx = p.x + 2.6; ty = p.y + 1;   anchor = 'start'; }
                else if (pos === 'top')  { tx = p.x;       ty = p.y - 2.4; anchor = 'middle'; }
                else /* bottom */        { tx = p.x;       ty = p.y + 4.5; anchor = 'middle'; }
                return (
                  <g key={'pin-'+i}>
                    <circle cx={p.x} cy={p.y} r={i === hubIdx ? 2.2 : 1.4} fill="var(--accent)" stroke="#fff" strokeWidth={i === hubIdx ? 0.5 : 0.35} />
                    <text x={tx} y={ty} fill="#fff" fontSize={i === hubIdx ? 2.8 : 2.4} fontWeight={i === hubIdx ? 800 : 600} textAnchor={anchor} style={{paintOrder: 'stroke', stroke: 'rgba(0,0,0,0.65)', strokeWidth: 0.55}}>{p.label}</text>
                  </g>
                );
              })}
            </>
          ) : (
            <text x="50" y="65" fill="rgba(255,255,255,0.4)" textAnchor="middle" fontSize="3">Karte lädt …</text>
          )}
        </svg>
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────
   GermanyClusterMap — clean map (no labels) + interactive legend list
───────────────────────────────────────── */
const GERMANY_VIEWBOX_CLUSTER = { w: 110, h: 130, projRight: 92 };

function GermanyClusterMap({ clusters = [], title, lead }) {
  const { lang } = (typeof useT === 'function') ? useT() : { lang: 'de' };
  const [data, setData] = React.useState(_germanyDataCache);
  const [hoverId, setHoverId] = React.useState(null);

  // Translatable UI strings + region descriptors (city names stay as proper nouns)
  const TX = {
    de: { hub: 'HUB', zentrale: 'Bernau Zentrale', sites: 'Einsatzorte', regions: 'Regionen', more: 'weitere', hint: 'Bewegen Sie den Mauszeiger über einen Punkt' },
    en: { hub: 'HUB', zentrale: 'Bernau HQ', sites: 'sites', regions: 'regions', more: 'more', hint: 'Hover over a point for details' },
    tr: { hub: 'MERKEZ', zentrale: 'Bernau Merkez', sites: 'saha', regions: 'bölge', more: 'daha', hint: 'Ayrıntılar için bir noktanın üzerine gelin' }
  };
  const REGIONS = {
    'Brandenburg · Hub':        { en: 'Brandenburg · Hub', tr: 'Brandenburg · Merkez' },
    'Hauptstadtregion':         { en: 'Capital region', tr: 'Başkent bölgesi' },
    'Süd-Brandenburg':          { en: 'Southern Brandenburg', tr: 'Güney Brandenburg' },
    'Prignitz & Uckermark':     { en: 'Prignitz & Uckermark', tr: 'Prignitz & Uckermark' },
    'Hamburg':                  { en: 'Hamburg', tr: 'Hamburg' },
    'Ostholstein':              { en: 'East Holstein', tr: 'Doğu Holstein' },
    'Westküste & Kieler Förde': { en: 'West coast & Kiel Fjord', tr: 'Batı kıyısı & Kiel Körfezi' },
    'Schwerin & Ostsee':        { en: 'Schwerin & Baltic Sea', tr: 'Schwerin & Baltık' },
    'Sachsen-Anhalt':           { en: 'Saxony-Anhalt', tr: 'Saksonya-Anhalt' },
    'Sachsen':                  { en: 'Saxony', tr: 'Saksonya' }
  };
  const L = TX[lang] || TX.de;
  const trRegion = (r) => (lang === 'de' ? r : (REGIONS[r] && REGIONS[r][lang]) || r);
  const NAMES = {
    'Berlin & Umland':            { en: 'Berlin & surroundings', tr: 'Berlin & çevresi' },
    'Spreewald & Niederlausitz':  { en: 'Spreewald & Lower Lusatia', tr: 'Spreewald & Aşağı Lusatia' },
    'Nord-Brandenburg':           { en: 'Northern Brandenburg', tr: 'Kuzey Brandenburg' },
    'Hamburg & Umland':           { en: 'Hamburg & surroundings', tr: 'Hamburg & çevresi' },
    'Lübeck-Bucht':               { en: 'Bay of Lübeck', tr: 'Lübeck Körfezi' },
    'Schleswig-Holstein':         { en: 'Schleswig-Holstein', tr: 'Schleswig-Holstein' },
    'Mecklenburg-Vorpommern':     { en: 'Mecklenburg-Vorpommern', tr: 'Mecklenburg-Vorpommern' },
    'Saale-Unstrut':              { en: 'Saale-Unstrut', tr: 'Saale-Unstrut' },
    'Dresden':                    { en: 'Dresden', tr: 'Dresden' }
  };
  const trName = (n) => (lang === 'de' ? n : (NAMES[n] && NAMES[n][lang]) || n);

  React.useEffect(() => {
    if (data) return;
    let cancelled = false;
    loadGermanyData().then(d => { if (!cancelled) setData(d); });
    return () => { cancelled = true; };
  }, []);

  const hubIdx = clusters.findIndex(c => c.hub);

  const projected = React.useMemo(() => {
    if (typeof window === 'undefined' || !window.d3 || !data) return null;
    const refGeo = data.states || data.country;
    if (!refGeo) return null;
    const w = GERMANY_VIEWBOX_CLUSTER.w, h = GERMANY_VIEWBOX_CLUSTER.h;
    const pad = 4;
    // Constrain country to the LEFT part of the viewBox, leaving room on the right for labels
    const proj = window.d3.geoMercator().fitExtent(
      [[pad, pad], [GERMANY_VIEWBOX_CLUSTER.projRight - pad, h - pad]],
      refGeo
    );
    const path = window.d3.geoPath(proj);
    const countryPath = data.country ? path(data.country) : null;
    const statePaths = data.states && data.states.features
      ? data.states.features.map(f => path(f))
      : null;
    const points = clusters.map(c => {
      const r = proj([c.lng, c.lat]) || [0, 0];
      return { ...c, x: r[0], y: r[1] };
    });
    return { countryPath, statePaths, points };
  }, [clusters, data]);

  const totalCities = clusters.reduce((s, c) => s + c.cities.length, 0);
  const hovered = hoverId ? clusters.find(c => c.id === hoverId) : null;

  return (
    <div className="qc-cmap">
      <div className="qc-cmap__stage">
        <svg className="qc-cmap__svg" viewBox={`0 0 ${GERMANY_VIEWBOX_CLUSTER.w} ${GERMANY_VIEWBOX_CLUSTER.h}`} xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
          {projected ? (
            <>
              {projected.statePaths && projected.statePaths.map((d, i) => (
                <path key={'st-'+i} d={d} fill="rgba(255,255,255,0.04)" stroke="rgba(255,255,255,0.16)" strokeWidth="0.22" strokeLinejoin="round" />
              ))}
              {projected.countryPath && (
                <path d={projected.countryPath} fill="none" stroke="rgba(255,255,255,0.5)" strokeWidth="0.4" strokeLinejoin="round" />
              )}
              {/* Hub pulse — anchored at actual hub position */}
              {hubIdx >= 0 && projected.points[hubIdx] && (
                <circle cx={projected.points[hubIdx].x} cy={projected.points[hubIdx].y} r="2" fill="var(--accent)" opacity="0.35">
                  <animate attributeName="r" from="2" to="6" dur="2s" repeatCount="indefinite" />
                  <animate attributeName="opacity" from="0.35" to="0" dur="2s" repeatCount="indefinite" />
                </circle>
              )}
              {/* Connections from hub to clusters — anchored at actual hub */}
              {hubIdx >= 0 && projected.points[hubIdx] && projected.points.map((p, i) => {
                if (i === hubIdx) return null;
                const isActive = hoverId === p.id;
                return (
                  <line key={'ln-'+i}
                    x1={projected.points[hubIdx].x} y1={projected.points[hubIdx].y}
                    x2={p.x} y2={p.y}
                    stroke="var(--accent)"
                    strokeWidth={isActive ? 0.55 : 0.26}
                    opacity={isActive ? 0.95 : (hoverId ? 0.15 : 0.35)}
                    strokeDasharray={isActive ? '0' : '0.7 1.2'}
                    style={{transition:'opacity 0.2s, stroke-width 0.2s'}}
                  />
                );
              })}
              {/* Cluster pins — non-hub first */}
              {projected.points.map((p, i) => {
                if (p.hub) return null;
                const count = p.cities.length;
                // Cap max radius at 3.8 so Berlin's 19-circle doesn't dominate
                const baseR = Math.max(1.4, Math.min(3.8, 1.0 + Math.sqrt(count) * 0.75));
                const isActive = hoverId === p.id;
                const r = isActive ? baseR + 1.0 : baseR;
                const dimmed = hoverId && !isActive;
                return (
                  <g key={'cl-'+i} style={{cursor:'pointer'}} onMouseEnter={() => setHoverId(p.id)} onMouseLeave={() => setHoverId(null)}>
                    <circle cx={p.x} cy={p.y} r={baseR + 3} fill="transparent" />
                    <circle cx={p.x} cy={p.y} r={r}
                      fill="var(--accent)"
                      stroke={isActive ? '#fff' : 'rgba(255,255,255,0.55)'}
                      strokeWidth={isActive ? 0.7 : 0.4}
                      opacity={dimmed ? 0.4 : 1}
                      style={{transition:'r 0.2s, opacity 0.2s, stroke-width 0.2s'}}
                    />
                    <text x={p.x} y={p.y + 0.85} fill="#fff" fontSize={Math.max(1.6, Math.min(2.4, r * 0.85))} fontWeight="800" textAnchor="middle" style={{pointerEvents:'none', opacity: dimmed ? 0.5 : 1, transition:'opacity 0.2s'}}>{count}</text>
                  </g>
                );
              })}
              {/* Hub marker — rendered LAST so it sits on top */}
              {projected.points.map((p, i) => {
                if (!p.hub) return null;
                const isActive = hoverId === p.id;
                const dimmed = hoverId && !isActive;
                // Offset marker NE so it sits clearly outside Berlin's cluster circle
                const offX = 8;
                const offY = -7;
                const dx = p.x + offX;
                const dy = p.y + offY;
                const r = isActive ? 2.0 : 1.4;
                const tx = dx + r + 1.4;
                return (
                  <g key={'cl-'+i} style={{cursor:'pointer'}} onMouseEnter={() => setHoverId(p.id)} onMouseLeave={() => setHoverId(null)}>
                    <circle cx={dx} cy={dy} r="6" fill="transparent" />
                    <rect x={tx} y={dy - 2.5} width="14" height="5" fill="transparent" />
                    {/* Leader line from real Bernau position to offset marker */}
                    <line x1={p.x} y1={p.y} x2={dx} y2={dy} stroke="var(--accent)" strokeWidth="0.45" opacity={dimmed ? 0.4 : 1} />
                    {/* Real-position indicator dot (tiny dot at actual Bernau) */}
                    <circle cx={p.x} cy={p.y} r="0.7" fill="#fff" stroke="var(--accent)" strokeWidth="0.35" opacity={dimmed ? 0.4 : 1} />
                    {/* Offset hub marker */}
                    <circle cx={dx} cy={dy} r={r + 1.0} fill="#1a1a1a" />
                    <circle cx={dx} cy={dy} r={r} fill="var(--accent)" stroke="#fff" strokeWidth="0.7" opacity={dimmed ? 0.5 : 1} style={{transition:'r 0.2s, opacity 0.2s'}} />
                    {/* Label to the right of marker — simple white text */}
                    <text x={tx} y={dy + 1.0}
                      fill="#fff" fontSize="2.2" fontWeight="700"
                      textAnchor="start"
                      style={{paintOrder:'stroke', stroke:'rgba(0,0,0,0.75)', strokeWidth:0.7, pointerEvents:'none', opacity: dimmed ? 0.6 : 1, transition:'opacity 0.2s'}}
                    >{L.zentrale}</text>
                  </g>
                );
              })}
            </>
          ) : (
            <text x="50" y="65" fill="rgba(255,255,255,0.4)" textAnchor="middle" fontSize="3">Karte lädt …</text>
          )}
        </svg>

        {/* Floating info panel — replaces the legend list */}
        <div className={'qc-cmap__panel' + (hovered ? ' is-active' : '')}>
          {hovered ? (
            <>
              <div className="qc-cmap__panel-meta">
                <span className="qc-cmap__panel-dot" style={{width: hovered.hub ? 12 : 16, height: hovered.hub ? 12 : 16}}>
                  {!hovered.hub && <span>{hovered.cities.length}</span>}
                </span>
                <span className="qc-cmap__panel-region">{trRegion(hovered.region)}</span>
                {hovered.hub && <span className="qc-cmap__panel-badge">{L.hub}</span>}
              </div>
              <div className="qc-cmap__panel-name">{trName(hovered.name)}</div>
              {hovered.cities.length > 1 && (
                <div className="qc-cmap__panel-cities">
                  {hovered.cities.slice(0, 12).join(' · ')}
                  {hovered.cities.length > 12 ? ` · +${hovered.cities.length - 12} ${L.more}` : ''}
                </div>
              )}
            </>
          ) : (
            <div className="qc-cmap__panel-default">
              <div className="qc-cmap__panel-stat"><strong>{totalCities}+</strong><span>{L.sites}</span></div>
              <div className="qc-cmap__panel-stat"><strong>{clusters.length}</strong><span>{L.regions}</span></div>
              <div className="qc-cmap__panel-hint">{L.hint}</div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { useInView, AnimatedNumber, Reveal, BeforeAfter, GermanyMap, GermanyClusterMap });

/* ─────────────────────────────────────────
   Auto-reveal: scan DOM for targeted elements, fade them in on scroll.
   Set --i per child for stagger; toggle .qc-reveal-on on parent when visible.
───────────────────────────────────────── */
(function setupAutoReveal() {
  if (typeof window === 'undefined') return;
  if (window.__qcAutoReveal) return;
  window.__qcAutoReveal = true;

  // Selectors of "container" elements — we add .qc-reveal-on once they enter view
  const containerSelectors = [
    '.hsec', '.hh', '.hpartner', '.hsvc-grid', '.svc-grid', '.prj-grid',
    '.hzert', '.hzert__seals', '.hvals', '.ab-narrative', '.ab-stats-box',
    '.prj-featured', '.prj-hero', '.page-head', '.hero', '.section',
    '.svchero', '.svc2-hero', '.svc2-intro', '.svc2-scope', '.svc2-process'
  ].join(',');

  function applyStagger(container) {
    // For grids, assign --i to immediate children
    const stagger = ['.hsvc-grid', '.svc-grid', '.prj-grid', '.hzert__seals', '.hpartner__stats', '.hvals', '.ab-stats-grid'];
    stagger.forEach(sel => {
      if (container.matches && container.matches(sel)) {
        Array.from(container.children).forEach((c, i) => c.style.setProperty('--i', i));
      } else {
        container.querySelectorAll(sel).forEach(grid => {
          Array.from(grid.children).forEach((c, i) => c.style.setProperty('--i', i));
        });
      }
    });
  }

  function observe(root = document) {
    const els = root.querySelectorAll(containerSelectors);
    if (!els.length) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach(en => {
        if (en.isIntersecting) {
          en.target.classList.add('qc-reveal-on');
          applyStagger(en.target);
          io.unobserve(en.target);
        }
      });
    }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
    els.forEach(el => io.observe(el));
  }

  // Re-scan whenever React re-renders something. Use MutationObserver as a safety net.
  let scheduled = false;
  function scheduleScan() {
    if (scheduled) return;
    scheduled = true;
    requestAnimationFrame(() => {
      scheduled = false;
      observe();
    });
  }
  document.addEventListener('DOMContentLoaded', scheduleScan);
  setTimeout(scheduleScan, 0);
  setTimeout(scheduleScan, 500);
  setTimeout(scheduleScan, 1500);

  // Watch the body for new content (route changes)
  const mo = new MutationObserver(() => scheduleScan());
  if (document.body) mo.observe(document.body, { childList: true, subtree: true });
  else document.addEventListener('DOMContentLoaded', () => mo.observe(document.body, { childList: true, subtree: true }));
})();
