// ConcertDetail — single concert: programme, performers, instrument, map, actions.
// Multilingual via `t`; real lid-painting imagery.
const detNS = window.RoyHeyneDesignSystem_df1161;
/* Solo-Gattungen: Orgelkonzert und Mittagsmusik spielt Roy immer allein — dort
   gibt es grundsätzlich keine Mitwirkenden, auch wenn im Portal versehentlich
   welche stehen. Dreisprachig geprüft. */
const _SOLO = /Orgelkonzert|Mittagsmusik|Organ Concert|Midday Music|Varhann[íi] koncert|Poledn[íi] hudba/i;

/* Aktionszeile — die Einrückung auf die Wertachse SCHNAPPT, sie schrumpft nicht:
   der Knopf landet immer auf einer der zwei etablierten Kanten (Wertachse 142 px
   oder Hauptachse 0), nie dazwischen. Passt die Zeile mit Vorspann nicht in die
   Spalte, fällt der Vorspann ganz weg — statt die Zeile umzubrechen oder den
   Knopf auf eine dritte Achse zu setzen. */
function Aktionszeile({ vorspann = 142, gap = 16, children, style }) {
  const ref = React.useRef(null);
  const [ein, setEin] = React.useState(true);
  React.useLayoutEffect(() => {
    const row = ref.current; if (!row || !row.parentElement) return;
    const mess = () => {
      const spalte = row.parentElement.clientWidth;
      const kinder = Array.from(row.children).filter((k) => k.getAttribute('data-vorspann') === null);
      let breite = 0; kinder.forEach((k) => { breite += k.getBoundingClientRect().width; });
      breite += gap * Math.max(0, kinder.length - 1);
      setEin(spalte >= vorspann + breite + 1);
    };
    mess();
    const ro = new ResizeObserver(mess); ro.observe(row.parentElement);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(mess);
    return () => ro.disconnect();
  }, [vorspann, gap]);
  return (
    <div ref={ref} style={{ display: 'flex', flexWrap: 'nowrap', alignItems: 'center', gap, ...style }}>
      {ein && <span aria-hidden="true" data-vorspann="" style={{ flex: '0 0 ' + vorspann + 'px' }} />}
      {children}
    </div>
  );
}

/* Eckwinkel — die Registermarken des Bildrahmens, auf den Knopf übertragen:
   vier L-Winkel statt eines geschlossenen Rechtecks. Eine Rahmensprache für
   Bild und Handlung. */
function Eckwinkel({ farbe = 'var(--messing)', laenge = 12, dicke = 1, versatz = 0 }) {
  const E = (a, b) => ({ position: 'absolute', width: laenge, height: laenge, [a]: versatz, [b]: versatz,
    ['border' + (a === 'top' ? 'Top' : 'Bottom')]: dicke + 'px solid ' + farbe,
    ['border' + (b === 'left' ? 'Left' : 'Right')]: dicke + 'px solid ' + farbe });
  return (<React.Fragment>
    <span aria-hidden="true" style={E('top', 'left')} /><span aria-hidden="true" style={E('top', 'right')} />
    <span aria-hidden="true" style={E('bottom', 'left')} /><span aria-hidden="true" style={E('bottom', 'right')} />
  </React.Fragment>);
}

// Orgelsommer — lebendiges Wochenprogramm + Archiv (aus window.RH_ORGELSOMMER).
function OrgelsommerWeek({ t }) {
  const cur = window.RH_ORGELSOMMER_CURRENT;
  const past = (window.RH_ORGELSOMMER || []).filter((w) => w.past).slice().reverse();
  const L = t.lang;
  const head = L === 'en' ? 'This week’s programme' : L === 'cs' ? 'Program tohoto týdne' : 'Programm dieser Woche';
  const arch = L === 'en' ? 'Programme archive' : L === 'cs' ? 'Archiv programů' : 'Programm-Archiv';
  const wk = (n) => (L === 'en' ? 'Week ' : L === 'cs' ? 'Týden ' : 'Woche ') + n;
  const fmt = (iso) => { try { return new Date(iso).toLocaleDateString(t.locale, { day: '2-digit', month: 'long' }); } catch (e) { return iso; } };
  if (!cur) return null;
  return (
    <div style={{ marginTop: 6 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginBottom: 14, flexWrap: 'wrap' }}>
        <h3 style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'var(--fs-24)', margin: 0, color: 'var(--text-strong)' }}>{head}.</h3>
        <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--messing-dunkel)' }}>{wk(cur.week)} · {fmt(cur.iso)}</span>
        <a href="#" onClick={(e) => e.preventDefault()} style={{ marginLeft: 'auto', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13-5)', color: 'var(--link)', textDecoration: 'none', whiteSpace: 'nowrap', padding: '12px 0', margin: '-12px 0 -12px auto' }}>
          {L === 'en' ? 'Programme sheet (PDF)' : L === 'cs' ? 'Programový list (PDF)' : 'Programmblatt (PDF)'} ↧
        </a>
      </div>
      {/* Steht die Werkfolge dieser Woche noch nicht, sagt die Seite das —
          eine leere Tafel mit Messingrand wäre ein Versprechen ohne Inhalt. */}
      {(cur.works && cur.works.length) ? (
      <div style={{ border: '1px solid var(--messing)', borderRadius: 'var(--radius-md)', background: 'var(--pergament-hell)', overflow: 'hidden' }}>
        {cur.works.map(([comp, work], i) => (
          <div key={i} style={{ display: 'grid', gridTemplateColumns: '1fr 1.3fr', gap: 18, padding: '13px 18px', borderBottom: i < cur.works.length - 1 ? '1px solid var(--border-hairline)' : 'none' }}>
            <span style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--text-muted)' }}>{comp}</span>
            <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-17)', fontStyle: 'italic', color: 'var(--text-strong)' }}>{work}</span>
          </div>
        ))}
      </div>
      ) : <ProgramPending t={t} />}
      {/* Nur Wochen mit Werkfolge gehören ins Archiv — leere Wochen wären
          eine Zeile, hinter der nichts steht. */}
      {past.filter((w) => w.works && w.works.length).length > 0 && <ProgrammArchiv past={past.filter((w) => w.works && w.works.length)} arch={arch} wk={wk} fmt={fmt} L={L} />}
    </div>
  );
}

/* Programm-Archiv — EINE Auskunft hinter EINEM Klick, wie die Chronik im
   Kalender. Vorher stapelten sich je Woche eigene Aufklapper: bei vierzehn
   Sommerwochen eine Kette von vierzehn geschlossenen Zeilen, die die Seite
   verlängert, ohne etwas zu zeigen. Jetzt sagt eine Zeile, wie viel dahinter
   liegt; geöffnet stehen die Wochen als lesbare Blätter da, ohne zweiten Klick. */
function ProgrammArchiv({ past, arch, wk, fmt, L }) {
  const [offen, setOffen] = React.useState(false);
  const isMobile = window.useIsMobile ? window.useIsMobile() : false;
  const wochen = L === 'en' ? 'weeks' : L === 'cs' ? 'týdnů' : 'Wochen';
  const zu = L === 'en' ? 'Close archive' : L === 'cs' ? 'Zavřít archiv' : 'Archiv schließen';
  return (
    <div style={{ marginTop: 30, borderTop: '1px solid var(--border-hairline)', paddingTop: 20 }}>
      <button type="button" onClick={() => setOffen(!offen)} aria-expanded={offen}
        className="rh-label"
        style={{ display: 'inline-flex', alignItems: 'center', gap: 10, minHeight: 44, padding: '0 2px', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--messing-dunkel)' }}>
        {offen ? zu : arch + ' · ' + past.length + ' ' + wochen}
        {/* Dieselbe Aufklapp-Sprache wie die Chronik im Kalender: ⌄, 180°,
            320 ms — eine Geste für eine Bedeutung, keine zweite Variante. */}
        <span aria-hidden="true" style={{ display: 'inline-block', color: 'var(--messing)', transform: offen ? 'rotate(180deg)' : 'none', transition: 'transform 320ms cubic-bezier(0.32, 0.72, 0, 1)' }}>⌄</span>
      </button>
      {offen && (
        <div className="rh-settle" style={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: 14 }}>
          {/* Mobil keine feste Spur: bei 390 px bliebe für die Werke zu wenig
              übrig. Dort stapelt die Woche über ihrem Programm. */}
          {past.map((w) => (
            <div key={w.week} style={isMobile
              ? { padding: '14px 0', borderTop: '1px solid var(--border-hairline)' }
              : { display: 'grid', gridTemplateColumns: 'minmax(0,132px) minmax(0,1fr)', gap: 20, padding: '14px 0', borderTop: '1px solid var(--border-hairline)', alignItems: 'first baseline' }}>
              <div style={{ display: isMobile ? 'flex' : 'block', alignItems: 'baseline', gap: 10, marginBottom: isMobile ? 6 : 0 }}>
                <div className="rh-label" style={{ color: 'var(--messing-dunkel)' }}>{wk(w.week)}</div>
                <div className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)', marginTop: isMobile ? 0 : 3 }}>{fmt(w.iso)}</div>
              </div>
              <div>
                {w.works.map(([comp, work], i) => (
                  <div key={i} style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-15-5)', lineHeight: 1.55, color: 'var(--text-body)' }}>
                    {comp} — <span style={{ fontStyle: 'italic', color: 'var(--text-strong)' }}>{work}</span>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function ProgramPending({ t }) {
  const L = t.lang;
  const head = L === 'en' ? 'The programme follows shortly' : L === 'cs' ? 'Program bude brzy zveřejněn' : 'Das Programm folgt in Kürze';
  const body = L === 'en' ? 'The programme for this concert is still being finalised. It will appear here in full as soon as it is confirmed — thank you for your patience.' : L === 'cs' ? 'Program tohoto koncertu se právě dokončuje. Jakmile bude potvrzen, objeví se zde v úplném znění — děkujeme za trpělivost.' : 'Das Programm für dieses Konzert wird gerade vorbereitet. Sobald es feststeht, erscheint es hier vollständig — vielen Dank für Ihre Geduld.';
  return (
    <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', background: 'var(--pergament-hell)', padding: '28px 30px', display: 'flex', gap: 18, alignItems: 'flex-start' }}>
      <span aria-hidden="true" style={{ color: 'var(--messing)', fontSize: 'var(--fs-24)', lineHeight: 1 }}>❧</span>
      <div>
        <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'var(--fs-22)', color: 'var(--text-strong)', marginBottom: 8 }}>{head}.</div>
        <p style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-16-5)', lineHeight: 1.7, color: 'var(--text-body)', margin: 0, maxWidth: '52ch' }}>{body}</p>
      </div>
    </div>
  );
}

// ── Hero-Scrim (Lesart 1: full-bleed Motiv, textverankerter Scrim) ─────────
// Das Kirchenjahr-Motiv liegt full-bleed hinter dem Hero (dokumentierte Hero-
// Ausnahme §15, damit es lebt). Darüber der KANONISCHE Hero-Scrim — exakt wie
// RanfftOrgan / PageHeroDark: 96°-Verlauf, rgba(18,16,12) (§15-Literal,
// identisch zu .rh-hero-scrim-m). SYSTEMKONSTANTE: der dichte Scrim-Endpunkt
// in der Textzone (x = 0) ist alpha ≥ 0,90 — bei 0,92 hält selbst ein rein-
// weißer Untergrund noch ~12:1 für Pergament-Text; Senkung bricht die AA-
// Garantie. Kein Laufzeit-Solver, keine Bildanalyse — deterministisch, statisch.

function ConcertDetail({ id, onNav, t }) {
  const { Button, ImagePlate, StatusDot, Ornament } = detNS;
  const [shareOpen, setShareOpen] = React.useState(false);
  // Zwei-Klick-Karte (Datenschutz §8): vor der Einwilligung wird KEINE Verbindung
  // zu Google aufgebaut — kein iframe, kein Vorschaubild, kein Preconnect. Erst
  // der Klick auf „Google Maps laden“ setzt den Rahmen; „Externe Karte schließen“
  // nimmt ihn wieder weg. Die Einwilligung wird nicht gespeichert (gilt je Ansicht).
  const [karteGeladen, setKarteGeladen] = React.useState(false);
  const _all = [...window.RH_DATA.concerts, ...(window.RH_DATA.past || [])];
  const c = _all.find((x) => x.id === id) || _all[0];
  // Kirchenjahr-Gemälde-Nachweis (nur wenn das Hero-Bild ein gemeinfreies Werk ist;
  //   Roys eigene Instrument-/Venue-Fotos tragen keinen Kunst-Nachweis).
  const _artBase = String(c.image || '').split('/').pop().split('?')[0];
  const _artCredit = (window.RH_ART_CREDIT || {})[_artBase] || null;
  // Sprachgerechte Anführungszeichen (Startseiten-Format): DE/CZ „…“, EN “…”.
  const _qOpen = t.lang === 'en' ? '\u201C' : '\u201E';
  const _qClose = t.lang === 'en' ? '\u201D' : '\u201C';
  const isMobile = window.useIsMobile();
  // Titel und Anlass exakt wie auf der Karte — eine Quelle, ein Ergebnis.
  const _NS = window.RoyHeyneDesignSystem_df1161 || {};
  const _anl = c.anlass ? t.f(c.anlass) : (() => { const a = _NS.KirchenjahrAnlass && _NS.KirchenjahrAnlass(c.iso, window.RH_confOf ? window.RH_confOf(c) : 'ev'); return a ? (a.label[t.lang] || a.label.de) : ''; })();
  const _kopf = _NS.Kartenkopf ? _NS.Kartenkopf(t.f(c.title), { place: t.f(c.place), city: t.f(c.city), instrument: t.f(c.instrument), anlass: _anl }) : t.f(c.title);
  const shareUrl = window.RH_STAMM.webUrl + '/konzerte/' + c.id;
  const shareText = `${t.f(c.title)} — ${t.dateLong(c.iso)}`;
  const shareOptions = [
    { label: 'Link', icon: '⧉', href: shareUrl },
    { label: 'E-Mail', icon: '✉', href: `mailto:?subject=${encodeURIComponent(t.f(c.title))}&body=${encodeURIComponent(shareText + ' · ' + shareUrl)}` },
    { label: 'WhatsApp', icon: '✆', href: `https://wa.me/?text=${encodeURIComponent(shareText + ' ' + shareUrl)}` },
    { label: 'Facebook', icon: 'f', href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}` },
  ];
  // Instrument-Angaben nur, wo sie WAHR sind: Roys historische Instrumente und die
  // Ranft-Orgel (415 Hz, ungleichstufig, mechanisch). Für fremde Orgeln (z. B. die
  // Jehmlich-Orgel der Kreuzkirche, 440 Hz) keine erfundenen Werte — lieber nichts.
  const _histInstr = /Ranft|Silbermann|Cembalo|Positiv|Ottavino|Jehmlich.*(1|7|8)/.test(t.f(c.instrument) || '') && !/Jehmlich/.test(t.f(c.instrument) || '');
  const specLines = c.organSpec || ((c.id || '').startsWith('geising') || _histInstr
    ? ['a′ = 415 Hz',
       (t.lang === 'en' ? 'unequal temperament' : t.lang === 'cs' ? 'nerovnoměrné ladění' : 'ungleichstufige Temperierung'),
       (t.lang === 'en' ? 'mechanical action' : t.lang === 'cs' ? 'mechanická traktura' : 'mechanische Traktur')]
    : null);
  // Mitwirkende NUR aus echten Portal-Daten — nie erfundene Ensembles. Ohne Eintrag
  //   spielt Roy allein (das ist der Normalfall seiner Dienste/Konzerte).
  const _fromPortal = (c.performers && c.performers.length) ? c.performers.map((p) => [p.name, p.role || '']) : null;
  // Roys Instrument aus Titel/Instrument/Programm ableiten: Cembalo nur wenn wirklich
  // Cembalo, beides nur bei ausdrücklicher Nennung — sonst schlicht „Orgel".
  const _txt = [(c.title && c.title.de) || '', (c.instrument && c.instrument.de) || '', (c.programme && c.programme.de) || ''].join(' ');
  const _hasCemb = /Cembalo|Harpsichord/i.test(_txt);
  const _hasOrgel = /Orgel|Organ|Ranft|Silbermann|Jehmlich/i.test(_txt);
  const _royRole = (_hasCemb && _hasOrgel) ? { de: 'Orgel & Cembalo', en: 'Organ & harpsichord', cs: 'Varhany & cembalo' }
    : _hasCemb ? { de: 'Cembalo', en: 'Harpsichord', cs: 'Cembalo' }
    : { de: 'Orgel', en: 'Organ', cs: 'Varhany' };
  // Orgelsommer (wiederkehrend): IMMER solo — Roy allein, oder die Wochen-Vertretung.
  const _osSub = c.recurring && window.RH_ORGELSOMMER_CURRENT && window.RH_ORGELSOMMER_CURRENT.substitute;
  const performers = c.recurring
    ? [[_osSub || 'Roy Heyne', { de: 'Orgel', en: 'Organ', cs: 'Varhany' }]]
    : (_fromPortal || [['Roy Heyne', _royRole]]);
  const hasEnsemble = !c.recurring && !!_fromPortal && !_SOLO.test(t.f(c.title) || '');

  // Eckdaten-Ledger — ein einziges, scannbares Band der Essentials, auf JEDER
  //   Veranstaltung identisch aufgebaut (Ive-Tech-Specs-Prinzip: Mikrolabel in
  //   Messing über kräftigem Wert). Datengetrieben; fehlende Felder fallen weg.
  // Uhrzeiten immer über die Hausregel (volle Stunde kurz) — nie ' Uhr' anhängen.
  const _zeit = (x) => (_NS.Zeitangabe ? _NS.Zeitangabe(x, t.lang) : x);
  const _venueHref = window.RH_maps(window.RH_mapsQueryFor(c));
  const facts = [];
  // Ort führt den Ledger an — der verortende Anker der Veranstaltung (steht oben).
  // Der Ledger trägt die KURZE, entdoppelte Form (Kirche / Ort auf zwei Zeilen) —
  // nicht den langen Untertitel aus venueFull, der den Ortsnamen erneut mitführt.
  facts.push({ k: t.f({ de: 'Ort', en: 'Venue', cs: 'Místo' }), v: (_NS.Ortszeile ? _NS.Ortszeile(t.f(c.place), t.f(c.city)) : t.f(c.place)), href: _venueHref });
  if (c.recurring && window.RH_ORGELSOMMER_SEASON) {
    const s = window.RH_ORGELSOMMER_SEASON;
    const a = s.fromIso ? new Date(s.fromIso).toLocaleDateString('de-DE', { day: 'numeric', month: 'long' }) : '';
    const b = s.toIso ? new Date(s.toIso).toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }) : '';
    facts.push({ k: t.f({ de: 'Termine', en: 'Dates', cs: 'Termíny' }), v: t.f({ de: (s.weekday || 'Mittwoch') + 's', en: 'Every ' + (s.weekday || 'Wednesday'), cs: 'Každou ' + (s.weekday || 'středu') }) });
    facts.push({ k: t.f({ de: 'Zeitraum', en: 'Season', cs: 'Období' }), v: a + '–' + b });
    facts.push({ k: t.f({ de: 'Uhrzeit', en: 'Time', cs: 'Čas' }), v: _zeit(s.time || c.time) });
  } else {
    facts.push({ k: t.f({ de: 'Datum', en: 'Date', cs: 'Datum' }), v: t.dateLong(c.iso), cap: true });
    facts.push({ k: t.f({ de: 'Uhrzeit', en: 'Time', cs: 'Čas' }), v: _zeit(c.time) });
  }
  if (c.admission) facts.push({ k: t.f({ de: 'Eintritt', en: 'Admission', cs: 'Vstup' }), v: t.f(c.admission) });

  return (
    <div>
      {/* Dark header band with the concert's lid-painting */}
      <section style={{ position: 'relative', overflow: 'hidden', color: 'var(--pergament-text)', background: 'var(--scrim-1)' }}>
        <img loading="eager" decoding="async" fetchpriority="high" src={c.image} alt={_artCredit ? (_artCredit.artist + ' · ' + t.f(_artCredit.title)) : ''} aria-hidden={_artCredit ? undefined : 'true'}
          style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 32%', opacity: 0.4 }} />
        <div aria-hidden="true" style={{ position: 'absolute', inset: 0, background: 'linear-gradient(96deg, rgba(18,16,12,0.92) 0%, rgba(18,16,12,0.62) 56%, rgba(18,16,12,0.34) 100%)' }} />
        <div className="rh-hero-scrim-m" aria-hidden="true" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }} />
        <div style={{ position: 'relative', maxWidth: 'var(--container)', margin: '0 auto', padding: isMobile ? '84px var(--gutter) 64px' : '120px var(--gutter) 78px' }}>
          {/* Rückweg — NUR mobil (dort der einzige sichtbare Rückweg). Am Desktop
              übernimmt die Kopfleiste; der Hero bleibt clean wie zuvor. */}
          {isMobile && (
            <a href="#" onClick={(e) => { e.preventDefault(); onNav('konzerte'); }}
              style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--text-on-inverse-muted)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', minHeight: 44, padding: '0 2px' }}>← {t('common.back_concerts')}</a>
          )}
          {/* Kirchenjahr-Eyebrow (keine Reihen): die liturgische Zeit aus dem
              Datum — dieselbe Quelle wie die Karte (Kartenkopf/KirchenjahrAnlass). */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap', marginTop: isMobile ? 14 : 0, marginBottom: 28 }}>
            <div className="rh-label" style={{ color: 'var(--pergament-text)' }}>{_anl}</div>
            {c.status && c.status !== 'geplant' && c.status !== 'vergangen' && (
              <StatusDot status={c.status} label={<span style={{ color: 'var(--text-on-inverse-muted)' }}>{t('status.' + c.status)}</span>} />
            )}
          </div>
          {/* Konzert-Hero-Variante (§06): Orgel-/Venue-Foto als Co-Lead → 1.1/0.9.
              `stretch` am Desktop, damit die linke Spalte auf Reihenhöhe wächst —
              nur so hat der Aktionsblock freien Raum, um sich an die Bildunterkante
              zu binden. Mobil bleibt `start` (gestapelter Fluss). */}
          <div style={{ display: 'grid', gridTemplateColumns: '1.1fr 0.9fr', gap: 48, alignItems: isMobile ? 'start' : 'stretch' }}>
            <div style={{ display: 'flex', flexDirection: 'column' }}>
              <h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2.6rem, 1.9rem + 2.6vw, 4rem)', lineHeight: 1.03, letterSpacing: '-0.015em', color: 'var(--pergament-text)', margin: 0 }}>{_kopf}.</h1>
              {!(c.works && c.works.length) && (
                <p style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-19)', lineHeight: 1.6, color: 'rgba(236,230,216,0.82)', marginTop: 20, marginBottom: 0, maxWidth: '46ch' }}>
                  {/* Liegt eine Werkliste vor, spricht die Tabelle weiter unten —
                      der Kopf wiederholt die Komponisten dann NICHT. Fehlt sie,
                      steht hier ehrlich, dass das Programm noch offen ist. */}
                  {t.f({ de: 'Das Programm wird bekanntgegeben.', en: 'The programme will be announced.', cs: 'Program bude oznámen.' })}
                </p>
              )}
              {/* Eckdaten — vertikales Spec-Sheet (Ive/Apple-Tech-Specs): feste
                  Label-Spur ⇒ paritätsfest in DE/EN/CZ (gleiche Geometrie
                  unabhängig von der Textlänge). Messing-Labels führen als Kante
                  den Blick nach unten; Werte präzise daran ausgerichtet. */}
              <dl style={{ margin: '40px 0 0', maxWidth: '48ch' }}>
                {facts.map((f, i) => (
                  <div key={i} style={isMobile
                    ? { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 18, padding: '14px 0', borderTop: '1px solid var(--hairline-on-inverse)', borderBottom: i === facts.length - 1 ? '1px solid var(--hairline-on-inverse)' : 'none' }
                    : { display: 'grid', gridTemplateColumns: '116px 1fr', gap: 26, alignItems: 'first baseline', padding: '14px 0', borderTop: '1px solid var(--hairline-on-inverse)', borderBottom: i === facts.length - 1 ? '1px solid var(--hairline-on-inverse)' : 'none' }}>
                    <dt style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12)', fontWeight: 600, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--messing)', lineHeight: 1.5, textAlign: isMobile ? 'left' : 'right', flex: isMobile ? 'none' : undefined, whiteSpace: 'nowrap' }}>{f.k}</dt>
                    <dd style={{ margin: 0, lineHeight: 1.5, textAlign: isMobile ? 'right' : 'left', flex: isMobile ? '1 1 auto' : undefined, minWidth: 0 }}>
                      {f.href ? (
                        <a href={f.href} target="_blank" rel="noopener" className="rh-navlink" style={{ ['--nav-hover']: 'var(--pergament-text)', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-16-5)', lineHeight: 1.42, color: 'var(--pergament-text)', textDecoration: 'none', padding: '11px 0' }}>{String(f.v).split(' · ').map((p, k) => k === 0 ? p : [<br key={k} />, p])} <span aria-hidden="true" style={{ color: 'var(--messing)' }}>↗</span></a>
                      ) : (
                        <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-16-5)', color: 'var(--pergament-text)', textTransform: f.cap ? 'capitalize' : 'none' }}>{String(f.v).split(' · ').map((p, k) => k === 0 ? p : [<br key={k} />, p])}</span>
                      )}
                    </dd>
                  </div>
                ))}
              </dl>
              {/* Die Handlungszeile ist der SCHLUSS des Ledgers, kein freier Block:
                  sie sitzt auf der Wertespalte (116px Label-Spur + 26px Rinne), so
                  dass die Messing-Kante der Labels ununterbrochen nach unten führt
                  und die Buttons unter den Werten stehen, auf die sie sich beziehen.
                  Mobil (Label links / Wert rechts) bleibt sie am linken Rand. */}
              {/* Aktionsblock — wie im Bild: der Kalenderknopf als Messing-Umriss
                  mit „Zum Kalender hinzufügen ↧“, „Teilen“ DANEBEN, nie darunter.
                  Die Einrückung auf die Wertachse schnappt (siehe Aktionszeile):
                  entweder Wertachse oder Hauptachse, nie eine dritte Kante.
                  `marginTop:auto` zieht den Block an den Spaltenfuß — auf breiten
                  Flächen, wo das Foto die höhere Spalte ist, schließt er damit auf
                  der Bildunterkante ab. Mobil: linker Rand, fester Abstand. */}
              <Aktionszeile vorspann={isMobile ? 0 : 142} style={{ marginTop: isMobile ? 30 : 'auto', paddingTop: isMobile ? 0 : 44 }}>
                <Button variant="inverse" iconAfter={<span>↧</span>} style={{ flex: 'none' }}
                  onClick={() => window.RH_icsDownload([c], c.id, t.lang)}>{t('detail.ical')}</Button>
                <Button variant="ghost" style={{ flex: 'none', color: 'var(--pergament-text)' }}
                  onClick={() => setShareOpen(true)}>{t('detail.share')}</Button>
              </Aktionszeile>
            </div>
            {/* Instrument-Foto: museale Kanon-Behandlung wie JEDER andere Hero
                (RanfftOrgan, PageHeroDark) — Rundbogen + Passepartout mit
                Eck-Registermarken. Ohne Plakette/Caption, damit der Orgelname
                allein in der Fließtext-Zeile steht (keine Namensdopplung). */}
            <ImagePlate tone="dark" src={c.organImage} arch ratio="4 / 5" zoom={c.portholeZoom} imgPosition={c.portholePos} />
          </div>
          {/* Gemäldenachweis — rechts unter dem Bildbereich, kursiv. */}
          {_artCredit && (
            <div style={{ marginTop: 30, textAlign: 'right', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-caption)', color: 'rgba(236,230,216,0.62)', fontStyle: 'italic', letterSpacing: '0.02em' }}>
              {_artCredit.artist} · <span>{_qOpen}{t.f(_artCredit.title)}{_qClose}</span>{_artCredit.jahr ? ', ' + _artCredit.jahr : ''}{_artCredit.museum ? ' · ' + _artCredit.museum : ''}
            </div>
          )}
        </div>
      </section>

      {/* Content */}
      <section style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: '72px var(--gutter) 96px',
        display: 'grid', gridTemplateColumns: '1.4fr 0.6fr', gap: 72 }}>
        <div>
          <h2 style={{ fontSize: 'var(--fs-32)', margin: '0 0 8px' }}>{window.RH_pt(t('detail.programme'))}</h2>
          {/* Die Programm-Notiz ist freiwillig. Fehlt sie, bleibt kein leerer
              Absatz stehen — die Auskunft übernimmt ProgramPending unten. */}
          {t.f(c.programme) ? (
            <p style={{ fontSize: 'var(--fs-19)', lineHeight: 1.7, color: 'var(--text-body)', marginBottom: 28 }}>{t.f(c.programme)}</p>
          ) : <div style={{ height: 20 }} />}
          {c.recurring ? (
            <OrgelsommerWeek t={t} />
          ) : (c.works && c.works.length && c.worksPublic !== false) ? (
            <div style={{ borderTop: '1px solid var(--border-hairline)' }}>
              {c.works.map(([comp, work], i) => (
                <div key={i} style={{ display: 'grid', gridTemplateColumns: '1fr 1.3fr', gap: 24, padding: '16px 0', borderBottom: '1px solid var(--border-hairline)' }}>
                  <div style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-15)', color: 'var(--text-muted)' }}>{comp}</div>
                  <div style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-18)', color: 'var(--text-strong)', fontStyle: 'italic' }}>{work}</div>
                </div>
              ))}
            </div>
          ) : (
            <ProgramPending t={t} />
          )}

          <Ornament mark="heart" style={{ margin: '56px auto' }} />

          {/* Recording — a real, playable still only for past concerts. Upcoming
              concerts get an honest note, never a play button that leads nowhere. */}
          {c.video && c.video.url ? (
          <figure style={{ margin: '0 0 8px', background: 'var(--pergament-hell)', border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', padding: 14 }}>
            <div className="rh-eyebrow" style={{ marginBottom: 12, paddingLeft: 2 }}>{t('video.eyebrow')}</div>
            <a href={c.video.url} target="_blank" rel="noopener" style={{ display: 'block', position: 'relative', aspectRatio: '16 / 9', borderRadius: 'var(--radius-sm)', overflow: 'hidden', outline: '1px solid rgba(156,124,67,0.28)', outlineOffset: -1, cursor: 'pointer', textDecoration: 'none' }}>
              <img loading="lazy" decoding="async" src={c.image} alt="" aria-hidden="true" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 38%', filter: 'saturate(0.96) contrast(1.02)' }} />
              <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(18,16,12,0.05), rgba(18,16,12,0.5))' }} />
              <span aria-hidden="true" style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: 66, height: 66, borderRadius: '50%', border: '2px solid var(--pergament-text)', display: 'grid', placeItems: 'center', paddingLeft: 4, fontSize: 'var(--fs-20)', color: 'var(--pergament-text)', background: 'rgba(26,24,19,0.35)' }}>▶</span>
              <div style={{ position: 'absolute', left: 14, bottom: 12, fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--pergament-text)' }}>{t('video.watch')}</div>
            </a>
          </figure>
          ) : (
            <div style={{ display: 'flex', gap: 14, alignItems: 'center', padding: '18px 22px', background: 'var(--pergament-hell)', border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)' }}>
              <span aria-hidden="true" style={{ color: 'var(--messing)', fontSize: 'var(--fs-20)', lineHeight: 1 }}>❧</span>
              <div>
                <div className="rh-eyebrow" style={{ marginBottom: 4 }}>{t('video.eyebrow')}</div>
                <div style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-15-5)', color: 'var(--text-muted)' }}>{t('video.empty')}</div>
              </div>
            </div>
          )}

          {hasEnsemble && (
          <React.Fragment>
              <h2 style={{ fontSize: 'var(--fs-32)', margin: '56px 0 18px' }}>{window.RH_pt(t('detail.performers'))}</h2>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
                {performers.map(([n, r], i) => (
                  <div key={i} style={{ display: 'flex', justifyContent: 'space-between', maxWidth: '46ch', paddingBottom: 12, borderBottom: '1px solid var(--border-hairline)' }}>
                    <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-18)', color: 'var(--text-strong)' }}>{n}</span>
                    <span style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--text-muted)' }}>{typeof r === 'string' ? r : t.f(r)}</span>
                  </div>
                ))}
              </div>
            </React.Fragment>
          )}
        </div>

        {/* Aside */}
        <aside style={{ display: 'flex', flexDirection: 'column', gap: 28, position: 'sticky', top: 88, alignSelf: 'start' }}>
          <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', padding: '22px 24px', background: 'var(--surface-card)' }}>
            <div className="rh-label" style={{ color: 'var(--messing-dunkel)', marginBottom: 14 }}>{t('detail.instrument')}</div>
            <div style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'var(--fs-22)', color: 'var(--text-strong)', lineHeight: 1.2 }}>{t.f(c.instrument)}</div>
            {specLines && (
              <div className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--text-muted)', marginTop: 12, lineHeight: 1.7 }}>
                {specLines.map((line, i) => <div key={i}>{line}</div>)}
              </div>
            )}
            {c.id.startsWith('geising') && (
              <a href="#" onClick={(e) => { e.preventDefault(); onNav('orgel'); }}
                style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--link)', display: 'inline-block', marginTop: 10, padding: '12px 0', margin: '-2px 0 -12px' }}>{t('nav.orgel')} →</a>
            )}
          </div>
          {/* Anfahrt — mit echter Maps-Vorschau. Der Ortsname steht im Ledger,
              hier trägt die Karte ihren eigenen Inhalt: die Lage im Bild. */}
          {(() => {
            const q = window.RH_mapsQueryFor(c);
            if (!q.trim()) return null;
            return (
              <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', padding: '22px 24px', background: 'var(--surface-card)' }}>
                <div className="rh-label" style={{ color: 'var(--messing-dunkel)', marginBottom: 14 }}>{t('detail.map')}</div>
                {/* Die Adresse steht IMMER als Text — auch ohne Karte (Datenschutz §8). */}
                <p style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--text-body)', margin: '0 0 12px', lineHeight: 1.5 }}>{q}</p>
                <div style={{ borderRadius: 'var(--radius-sm)', overflow: 'hidden', border: '1px solid var(--border-hairline)', background: 'var(--nachtschiefer)', position: 'relative', minHeight: 190 }}>
                  {karteGeladen ? (
                    <iframe title={t('detail.map')} loading="lazy" referrerPolicy="no-referrer-when-downgrade"
                      src={'https://www.google.com/maps?q=' + encodeURIComponent(q) + '&z=15&output=embed'}
                      style={{ display: 'block', width: '100%', height: 190, border: 0, filter: 'saturate(0.9) contrast(1.02)' }} />
                  ) : (
                    /* Lokaler Platzhalter: Pergament-Ton auf Nachtschiefer, kein
                       fremdes Bild. Der Knopf trägt die Einwilligung. */
                    <div style={{ display: 'grid', placeItems: 'center', gap: 10, height: 190, padding: 18, textAlign: 'center' }}>
                      <span style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12-5)', color: 'rgba(236,230,216,0.72)', lineHeight: 1.55, maxWidth: '30ch' }}>
                        {t.f({ de: 'Die Karte lädt erst nach Ihrem Klick Inhalte von Google.', en: 'The map loads content from Google only after your click.', cs: 'Mapa načte obsah od Googlu až po vašem kliknutí.' })}
                      </span>
                      <button type="button" onClick={() => setKarteGeladen(true)}
                        style={{ minHeight: 44, padding: '0 16px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--messing)', background: 'transparent', color: 'var(--messing)', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', letterSpacing: '0.02em', cursor: 'pointer' }}>
                        {t.f({ de: 'Google Maps laden', en: 'Load Google Maps', cs: 'Načíst Google Maps' })}
                      </button>
                    </div>
                  )}
                </div>
                <div style={{ display: 'flex', gap: 18, flexWrap: 'wrap', alignItems: 'center' }}>
                  <a href={window.RH_maps(q)} target="_blank" rel="noopener"
                    style={{ display: 'inline-flex', alignItems: 'center', gap: 8, minHeight: 44, fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', fontWeight: 500, color: 'var(--link)', textDecoration: 'none', paddingTop: 6 }}>
                    {t.lang === 'en' ? 'Open in Google Maps' : t.lang === 'cs' ? 'Otevřít v Google Maps' : 'In Google Maps öffnen'} <span aria-hidden="true" style={{ color: 'var(--messing)' }}>↗</span>
                  </a>
                  {karteGeladen && (
                    <button type="button" onClick={() => setKarteGeladen(false)}
                      style={{ minHeight: 44, background: 'none', border: 'none', padding: '6px 0 0', cursor: 'pointer', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)' }}>
                      {t.f({ de: 'Externe Karte schließen', en: 'Close external map', cs: 'Zavřít externí mapu' })}
                    </button>
                  )}
                </div>
              </div>
            );
          })()}
        </aside>
      </section>

      {/* Share — a branded dialog, not a system sheet */}
      {shareOpen && (
        <div onClick={() => setShareOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 60, background: 'rgba(18,16,12,0.62)', backdropFilter: 'blur(3px)', display: 'grid', placeItems: 'center', padding: 24 }}>
          <div onClick={(e) => e.stopPropagation()} style={{ width: 'min(440px, 100%)', background: 'var(--surface-card)', border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', overflow: 'hidden', boxShadow: 'var(--elev-4)' }}>
            <div style={{ background: 'var(--nachtschiefer)', color: 'var(--pergament-text)', padding: '18px 24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <span style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'var(--fs-22)' }}>{t('detail.share')}.</span>
              <button onClick={() => setShareOpen(false)} aria-label="×" style={{ background: 'none', border: 'none', color: 'var(--pergament-text)', fontSize: 'var(--fs-24)', lineHeight: 1, cursor: 'pointer' }}>×</button>
            </div>
            <div style={{ padding: '22px 24px 26px' }}>
              <div style={{ display: 'flex', gap: 14, alignItems: 'center', marginBottom: 22, paddingBottom: 18, borderBottom: '1px solid var(--border-hairline)' }}>
                <span style={{ width: 54, height: 54, borderRadius: '50%', overflow: 'hidden', border: '1.5px solid var(--messing)', flex: 'none' }}>
                  <img loading="lazy" decoding="async" src={c.porthole} alt="" aria-hidden="true" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: c.portholePos || 'center', transform: c.portholeZoom && c.portholeZoom !== 1 ? 'scale(' + c.portholeZoom + ')' : undefined }} />
                </span>
                <span style={{ minWidth: 0 }}>
                  <span style={{ display: 'block', fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'var(--fs-19)', color: 'var(--text-strong)', lineHeight: 1.2 }}>{_kopf}.</span>
                  <span className="rh-num" style={{ display: 'block', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)', marginTop: 2, textTransform: 'capitalize' }}>{t.dateLong(c.iso)} · {c.time} {t('common.uhr')}</span>
                </span>
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
                {shareOptions.map((o) => (
                  <a key={o.label} href={o.href} target="_blank" rel="noopener" style={{ textDecoration: 'none', textAlign: 'center' }}>
                    <span style={{ display: 'grid', placeItems: 'center', width: 50, height: 50, borderRadius: '50%', margin: '0 auto 7px', border: '1px solid var(--border-hairline)', color: 'var(--messing-dunkel)', fontSize: 'var(--fs-18)', fontFamily: o.label === 'Facebook' ? 'var(--font-glyph-serif)' : 'inherit', fontWeight: o.label === 'Facebook' ? 700 : 400 }}>{o.icon}</span>
                    <span style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12)', color: 'var(--text-muted)' }}>{o.label}</span>
                  </a>
                ))}
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { ConcertDetail });
