// ContentPages — Vita, Edition, Instruments, and a graceful "in preparation"
// page for Contact/others. Page-specific prose is inlined as {de,en,cs} and
// resolved with t.f(); shared chrome strings come from the dictionary.
const cpNS = window.RoyHeyneDesignSystem_df1161;
const F = (de, en, cs) => ({ de, en, cs });

function PageHeroDark({ eyebrow, title, lead, image, bgImage, plate, caption, imgPos = 'center', slot, t }) {
  const { ImagePlate } = cpNS;
  const so = (slot && window.RH_slot) ? window.RH_slot(slot, { src: image, imgPosition: imgPos }) : null;
  const heroSrc = so ? so.src : image;
  const bgSrc = bgImage || heroSrc;
  return (
    <section style={{ position: 'relative', overflow: 'hidden', color: 'var(--pergament-text)', background: 'var(--scrim-1)' }}>
      {/* Seiten-Kopfbild: sichtbar ab dem ersten Bildaufbau, darum kein Aufschub. */}
      <img loading="eager" decoding="async" fetchpriority="high" src={bgSrc} alt="" aria-hidden="true"
        style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 36%', opacity: 0.32 }} />
      <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(96deg, rgba(18,16,12,0.92) 0%, rgba(18,16,12,0.62) 58%, rgba(18,16,12,0.34) 100%)' }} />
      <div style={{ position: 'relative', maxWidth: 'var(--container)', margin: '0 auto', padding: '120px var(--gutter) 78px',
        display: 'grid', gridTemplateColumns: '1.25fr 0.75fr', gap: 56, alignItems: 'start' }}>
        <div className="rh-stagger">
          <div className="rh-eyebrow rh-eyebrow--dark" style={{ marginBottom: 18 }}>{eyebrow}</div>
          <h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2.6rem, 1.8rem + 3vw, 4.4rem)', lineHeight: 1.02, color: 'var(--pergament-text)', margin: 0 }}>{window.RH_pt(title)}</h1>
          {lead && <p style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-20)', lineHeight: 1.7, color: 'rgba(236,230,216,0.86)', maxWidth: '46ch', marginTop: 22, minHeight: 'calc(1.7em * 2)', textWrap: 'pretty' }}>{lead}</p>}
        </div>
        {image && <ImagePlate className="rh-plate-in" tone="dark" src={heroSrc} slot={slot} arch ratio="4 / 5" plate={plate} caption={caption} imgPosition={so ? so.imgPosition : imgPos} zoom={so ? so.zoom : 1} />}
      </div>
    </section>
  );
}

/* ---------------- Vita ---------------- */
function VitaPage({ onNav, t }) {
  const { Ornament, ImagePlate, SectionHeading } = cpNS;
  const paras = [
    F(
      'Geboren in Marienberg/Erzgebirge, wo er den ersten Unterricht auf Tasteninstrumenten, Waldhorn und Blockflöte bei KMD Karl-Heinz Gnida † erhielt, verfolgte er das Studium an der Hochschule für Kirchenmusik Dresden mit den Hauptfächern Orgel (Merit Eichhorn, KMD Dietrich Wagler), Klavier (Prof. Sabine Bräutigam) und Chorleitung (Prof. Dr. Christfried Brödel / Prof. Matthias Geissler), außerdem Cembalo/Generalbass (Raphael Alpermann) und Blockflöte (Gudrun Strohhäcker). Das Studium wurde mit dem Diplom \u203aKirchenmusik B\u2039 abgeschlossen. Anschließend folgte ein 2-jähriger Kantorendienst an der Hauptkirche St. Johannis zu Plauen. Konzertante Höhepunkte dieser Kantorentätigkeit stellten die Aufführung der »Brockes-Passion« Händels und die Durchführung der wöchentlichen Veranstaltungsreihe des »Orgelsommers« dar.',
      'Native to the city of Marienberg \u2013 the heart of the South-Saxon Erzgebirge \u2013, where he received his first training on keyboard instruments, the French horn, and on the recorder from KMD Karl-Heinz Gnida †, Roy Heyne commenced his course of studies at Dresden »Hochschule für Kirchenmusik« with organ music (Merit Eichhorn, KMD Dietrich Wagler), piano performance (prof. Sabine Bräutigam), and musical direction (prof. Dr. Christfried Brödel/prof. Matthias Geissler) as his major subjects. Further focal point was the instruction on both harpsichord as well as basso continuo playing by Raphael Alpermann, and on the \u2018dulcet\u2019 flute by Gudrun Strohhäcker. Roy Heyne finished his studies with the diploma in church music on B level (i.e. B.Mus.). Subsequently, he occupied the position of Kantor at St. Johannis, main church of the city of Plauen, for a period of two years. Concertante culminations of which were the rendering of the Brockes Passion, and the execution of the »Orgelsommer« \u2013 a weekly recurring series of concerts.',
      'Narozen v Marienbergu/Krušnohoří, kde získal první výuku na klávesových nástrojích, lesním rohu a zobcové flétně u KMD Karl-Heinze Gnidy †, absolvoval studium na Vysoké škole chrámové hudby v Drážďanech s hlavními obory varhany (Merit Eichhorn, KMD Dietrich Wagler), klavír (prof. Sabine Bräutigam) a sbormistrovství (prof. Dr. Christfried Brödel / prof. Matthias Geissler), mimoto cembalo/generální bas (Raphael Alpermann) a zobcová flétna (Gudrun Strohhäcker). Studium zakončil diplomem \u203aChrámová hudba B\u2039. Poté následovaly dva roky kantorské služby v hlavním kostele sv. Jana v Plavně. Koncertními vrcholy této kantorské činnosti byly provedení Händelových »Brockesových pašijí« a uskutečnění týdenní koncertní řady »Orgelsommer«.'
    ),
    F(
      'Das Studium zum Master of Music (M.Mus.) für »Historische Tasteninstrumente/Alte Musik« mit Schwerpunktfach Cembalo absolvierte er bei Prof. Ludger Rémy † und Elina Albach an der Hochschule für Musik Carl Maria von Weber Dresden. Im Rahmen seiner Masterarbeit, welche sich dem böhmischen Barockkomponisten Joseph Anton Sehling (1710\u20131756) widmet, edierte und präsentierte er dessen »Messe Pastorale Beati mites«.',
      'Perfecting his education as Master of early Music (M.Mus.), he graduated from »Hochschule für Musik Carl Maria von Weber Dresden«, having majored in harpsichord playing under prof. Ludger Rémy † and Elina Albach in the early music/period keyboard instruments master\u2019s programme. As part of his master\u2019s thesis, which is dedicated to Bohemian Baroque composer Joseph Anton Sehling (1710\u20131756), he edited and presented his »Messe Pastorale Beati mites«.',
      'Magisterské studium (M.Mus.) oboru »Historické klávesové nástroje / Stará hudba« se zaměřením na cembalo absolvoval u prof. Ludgera Rémyho † a Eliny Albach na Vysoké škole hudební Carla Marii von Webera v Drážďanech. V rámci své magisterské práce věnované českému baroknímu skladateli Josephu Antonu Sehlingovi (1710\u20131756) připravil a uvedl jeho »Messe Pastorale Beati mites«.'
    ),
    F(
      'Roy Heyne ist Kantor im Ev.-Luth. Kirchspiel Osterzgebirge - Region Altenberg.',
      'Roy Heyne is Kantor of the Ev. Lutheran Parish of Osterzgebirge - Region Altenberg.',
      'Roy Heyne je kantorem ev.-luter. farnosti Osterzgebirge - Region Altenberg.'
    ),
  ];
  return (
    <div>
      <PageHeroDark t={t} eyebrow={t('nav.vita')}
        title={t('teaser.vita_title')}
        lead={t.f(F('Aufgewachsen im Erzgebirge, ausgebildet in Dresden — heute Kantor an den historischen Orgeln zwischen Sachsen und Böhmen.', 'Grew up in the Ore Mountains, trained in Dresden — today Kantor at the historic organs between Saxony and Bohemia.', 'Vyrostl v Krušnohoří, vystudoval v Drážďanech — dnes kantor u historických varhan mezi Saskem a Čechami.'))}
        image={window.RH_IMG.portrait} bgImage={window.RH_IMG.ranftNave} slot="vita-portrait" caption={t.f({ de: 'Roy Heyne · Foto: Daniel Förster', en: 'Roy Heyne · Photo: Daniel Förster', cs: 'Roy Heyne · Foto: Daniel Förster' })} imgPos="center 20%" />
      <section style={{ maxWidth: '760px', margin: '0 auto', padding: '88px var(--gutter) 40px' }}>
        {/* AUSNAHME (nur Vita): Biografie-Absätze fließen pro Sprache natürlich, statt
            im Parity-Overlay (RH_ParityText) die Höhe der längsten Sprache zu reservieren.
            Grund: die langen, unterschiedlich langen Absätze erzeugten im Overlay riesige
            Leerräume unter kürzeren Sprachen. Hier zählt gleichmäßig moderater Absatz-
            abstand mehr als knotengleiche Geometrie. Bewusste, dokumentierte Ausnahme —
            siehe PARITY.md. Gilt NUR für diese Textspalte. */}
        {paras.map((p, i) => React.createElement('p', { key: i,
          className: i === 0 ? 'rh-dropcap' : undefined,
          style: { fontSize: 'var(--fs-20)', lineHeight: 1.8, color: 'var(--text-body)', margin: 0, marginBottom: i < paras.length - 1 ? 22 : 0 } },
          t.f(p)))}
        <Ornament mark="leaf" style={{ margin: '64px auto 56px' }} />
        {/* Roy in der ersten Person — die menschliche Stimme hinter der Arbeit.
            Das Zitat ist lang; 34ch hätten sechs kurze Zeilen ergeben. 46ch gibt
            ihm Raum, ohne die Mitte zu verlassen (zentriert, ausgewogener
            Umbruch — keine einsame letzte Zeile). */}
        <blockquote style={{ margin: 0, textAlign: 'center', maxWidth: 'min(58ch, 100%)', marginInline: 'auto' }}>
          {window.RH_ParityText({ as: 'p', lang: t.lang,
            all: F('„Die reizvolle und heitere Stimmung und Ausstrahlung der böhmischen Pastorellen, welche den Zuhörenden unmittelbar erreicht, möge stets die Freude der Hirten über die Geburt Jesu Christi weitergeben.“',
                   '“May the charming, serene mood of the Bohemian pastorellas, which reaches the listener directly, always pass on the shepherds’ joy at the birth of Jesus Christ.”',
                   '„Kéž půvabná a radostná nálada českých pastorel, která zasahuje posluchače bezprostředně, stále předává radost pastýřů z narození Ježíše Krista.“'),
            textStyle: { fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.4rem, 1.05rem + 1.2vw, 1.95rem)', lineHeight: 1.35, color: 'var(--text-strong)', textWrap: 'balance' } })}
          <footer className="rh-label" style={{ color: 'var(--messing-dunkel)', marginTop: 18 }}>Roy Heyne</footer>
        </blockquote>
      </section>
      {(() => {
        // Roys eigenes Wort (im Gespräch mit der Sächsischen Zeitung, 2026) — KEINE
        //   Pressestimme, sondern sein künstlerisches Credo. Bewusst als solches
        //   ausgewiesen; echte Ohlasy/Pressezitate pflegt Roy im Portal (Presse).
        const credo = t.f({
          de: '„Ich versuche immer wieder, Werke zu finden und erklingen zu lassen, die der heutigen Musikwelt noch nicht erschlossen sind und in Archiven schlummern.“',
          en: '“Again and again I try to find and bring to life works still unknown to today’s musical world — pieces that slumber in archives.”',
          cs: '„Stále znovu se snažím nacházet a rozeznívat díla, která dnešní hudební svět ještě nezná — skladby danné v archivech.“',
        });
        return (
          <section style={{ position: 'relative', overflow: 'hidden', background: 'var(--nachtschiefer)' }}>
            <img loading="lazy" decoding="async" src="/assets/images/img-06.jpg" alt="" aria-hidden="true" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', opacity: 0.22, filter: 'saturate(0.85)' }} />
            <div aria-hidden="true" style={{ position: 'absolute', inset: 0, background: 'radial-gradient(120% 90% at 50% 30%, rgba(26,23,18,0.55) 0%, rgba(26,23,18,0.82) 62%, rgba(26,23,18,0.95) 100%)' }} />
            <div style={{ position: 'relative', maxWidth: 'var(--container)', margin: '0 auto', padding: 'clamp(64px, 8vw, 120px) var(--gutter)' }}>
              <div className="rh-label" style={{ color: 'var(--messing)', marginBottom: 'clamp(32px, 4vw, 52px)', textAlign: 'center' }}>{t.f({ de: 'In eigenen Worten', en: 'In his own words', cs: 'Vlastními slovy' })}</div>
              <figure style={{ margin: 0, textAlign: 'center', display: 'grid', justifyItems: 'center', gap: 18, maxWidth: '54ch', marginInline: 'auto' }}>
                <span aria-hidden="true" style={{ fontFamily: 'var(--font-display)', fontSize: 'clamp(3rem, 2rem + 4vw, 5rem)', lineHeight: 0.6, color: 'var(--messing)', opacity: 0.5 }}>”</span>
                <blockquote style={{ margin: 0, fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.4rem, 1.05rem + 1.2vw, 2rem)', lineHeight: 1.36, color: 'var(--pergament)', textWrap: 'balance' }}>{credo}</blockquote>
                <span aria-hidden="true" style={{ width: 44, height: 1, background: 'var(--messing)', opacity: 0.6 }} />
                <figcaption className="rh-label" style={{ color: 'rgba(246,239,217,0.72)' }}>{t.f({ de: 'Roy Heyne · im Gespräch mit der Sächsischen Zeitung, 2026', en: 'Roy Heyne · in conversation with the Sächsische Zeitung, 2026', cs: 'Roy Heyne · v rozhovoru pro Sächsische Zeitung, 2026' })}</figcaption>
              </figure>
            </div>
          </section>
        );
      })()}
      {(() => {
        // Pressestimmen aus dem Portal (rh_entities.presse). Roys Eintr\u00e4ge sind einsprachig;
        //   nur die Rahmen-Beschriftung ist dreisprachig. Ohne Eintr\u00e4ge bleibt die Fl\u00e4che leer.
        // Grundstand aus den Konzerten (data.js → RH_PRESSE_SEED), danach aus
        // dem Register. Vorher war die Vorgabe leer, und die Sektion verschwand —
        // obwohl echte Pressestimmen an den Konzerten hingen.
        const PRESSE_DEFAULT = window.RH_PRESSE_SEED || [];
        const presse = (window.RH_entities ? window.RH_entities('presse', PRESSE_DEFAULT) : PRESSE_DEFAULT).filter((p) => p && (p.zitat || '').trim());
        if (!presse.length) return null;
        return (
          <section style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: '110px var(--gutter) 24px' }}>
            <SectionHeading label={t.f({ de: 'Presse', en: 'Press', cs: 'Tisk' })} title={t.f({ de: 'Pressestimmen', en: 'Press voices', cs: 'Ohlasy tisku' })} style={{ marginBottom: 'clamp(28px, 4vw, 48px)' }} />
            <div style={{ display: 'grid', gridTemplateColumns: presse.length > 1 ? 'repeat(2, minmax(0, 1fr))' : '1fr', gap: 'clamp(24px, 3vw, 40px)', alignItems: 'start' }}>
              {presse.map((p, i) => (
                <figure key={p.id != null ? p.id : i} style={{ margin: 0, minWidth: 0, display: 'grid', gap: 14, padding: 'clamp(24px, 3vw, 34px)', background: 'var(--pergament-warm)', border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)' }}>
                  <span aria-hidden="true" style={{ fontFamily: 'var(--font-display)', fontSize: '2.6rem', lineHeight: 0.5, color: 'var(--messing)', opacity: 0.5 }}>”</span>
                  <blockquote style={{ margin: 0, fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.2rem, 1rem + 0.7vw, 1.55rem)', lineHeight: 1.4, color: 'var(--text-strong)', textWrap: 'pretty' }}>{p.zitat}</blockquote>
                  <figcaption className="rh-label" style={{ color: 'var(--messing-dunkel)' }}>{[p.quelle, p.datum].filter(Boolean).join(' · ')}</figcaption>
                </figure>
              ))}
            </div>
          </section>
        );
      })()}
      <section style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: '24px var(--gutter) 110px' }}>
        <SectionHeading label={t.f({ de: 'Bilder', en: 'Images', cs: 'Obrazy' })} title={t.f({ de: 'Aus Werkstatt und Kirchenraum', en: 'From workshop and church', cs: 'Z dílny a kostela' })} style={{ marginBottom: 'clamp(28px, 4vw, 48px)' }} />
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 'clamp(24px, 3vw, 40px)', alignItems: 'start' }}>
          {[['img-06.jpg', window.RH_PLATE('cembalo'), t.f({ de: 'Deckelgemälde · Jessica Zappe', en: 'Lid painting · Jessica Zappe', cs: 'Malba na víku · Jessica Zappe' })], ['orgeln/ranft-08.jpg', window.RH_PLATE('ranft'), t.f({ de: 'Am Spieltisch der Ranft-Orgel', en: 'At the Ranft organ console', cs: 'U hracího stolu Ranftových varhan' })], ['img-23.avif', window.RH_PLATE('positiv'), t.f({ de: 'Orgelpositiv', en: 'Bohemian positive organ', cs: 'České varhanní pozitiv' })]].map(([f, no, cap], i) => (
            <ImagePlate key={i} src={'/assets/images/' + f} ratio="4 / 5" arch caption={cap} captionLines={1} />
          ))}
        </div>
      </section>
    </div>
  );
}

/* ---------------- Edition ---------------- */
function EditionPage({ onNav, t }) {
  const { Button, Badge, ImagePlate } = cpNS;
  // Nichts hartkodiert: der Grundstand steht in data.js (RH_EDITION_SEED) und
  // speist auch das Portal-Register. Was hier erscheint, kann Roy dort ändern.
  const EDN_DEFAULT = window.RH_EDITION_SEED || [];
  const editions = window.RH_entities ? window.RH_entities('editionen', EDN_DEFAULT) : EDN_DEFAULT;
  const primary = editions[0] || EDN_DEFAULT[0] || {};
  const ednCover = primary.cover || (window.RH_IMG && window.RH_IMG.editionCover);
  const ednSlot = window.RH_slot ? window.RH_slot('edition-cover', { src: ednCover }) : null;
  // Ein Link zählt nur, wenn er ein Ziel hat: http(s) davor, sonst nichts.
  const _rezension = (() => {
    const u = String((primary && primary.rezension) || '').trim();
    if (!u) return '';
    return /^https?:\/\//i.test(u) ? u : 'https://' + u.replace(/^\/+/, '');
  })();
  // Roy angemeldet? Der SERVER entscheidet — GET /api/auth/me, und dort
  // wird die Signatur des Access-Tokens geprueft (functions/api/auth/).
  // Vorher stand hier die Marke rh_portal_sitzung aus dem Browser: die
  // kann jeder setzen, sie war eine Anzeige und kein Schutz.
  // Einmal je Seitenaufruf gefragt und in window.RH_ANGEMELDET gemerkt,
  // damit nicht jede Ansicht erneut fragt. Faellt die Antwort aus, gilt
  // „nicht angemeldet" — im Zweifel kein Knopf, nie ein falscher.
  const [_angemeldet, _setAngemeldet] = React.useState(false);
  React.useEffect(() => {
    let lebt = true;
    if (!window.RH_ANGEMELDET) {
      window.RH_ANGEMELDET = fetch('/api/auth/me', { credentials: 'same-origin' })
        .then((r) => r.ok).catch(() => false);
    }
    window.RH_ANGEMELDET.then((an) => { if (lebt) _setAngemeldet(an); });
    return () => { lebt = false; };
  }, []);
  const descParas = [
    F(
      'Die Edition versammelt 17 Pastoralstücke aus der zweiten Hälfte des 18. Jahrhunderts für Orgel bzw. Cembalo (oder andere Tasteninstrumente), überliefert in böhmischen Manuskripten. Ursprünglich für die Weihnachtszeit bestimmt, sind die Pastorellen — Hirtenstücke — von Franz Xaver Brixi, Jiří Ignác Linek, Johann Hladky und Jakub Jan Ryba weitgehend manualiter ausführbar und von gemäßigtem Schwierigkeitsgrad. Ihre heitere, unmittelbare Ausstrahlung erreicht Spieler wie Zuhörer sogleich; so eignen sie sich auch über die Weihnachtszeit hinaus, für das Konzert wie für das häusliche Musizieren.',
      'The edition gathers 17 pastoral pieces from the second half of the 18th century for organ or harpsichord (or other keyboard instruments), preserved in Bohemian manuscripts. Originally intended for Christmas, these pastorellas — shepherd pieces — by Franz Xaver Brixi, Jiří Ignác Linek, Johann Hladky and Jakub Jan Ryba are largely playable manualiter and of moderate difficulty. Their cheerful, immediate charm reaches players and listeners at once; they suit occasions well beyond Christmas, in concert as much as domestic music-making.',
      'Edice shromažďuje 17 pastorálních skladeb z druhé poloviny 18. století pro varhany či cembalo (nebo jiné klávesové nástroje), dochovaných v českých rukopisech. Původně určené pro vánoční čas jsou tyto pastorely — pastýřské skladby — od Franze Xavera Brixiho, Jiřího Ignáce Linka, Johanna Hladkého a Jakuba Jana Ryby většinou proveditelné manuálově a mají mírnou obtížnost. Jejich veselé, bezprostřední vyznění zasáhne hráče i posluchače ihned; hodí se tak i mimo vánoční čas, do koncertu i k domácímu muzicírování.'
    ),
    F(
      'Die Ausgabe gliedert sich in drei Teile: Einleitung, Notenteil und Kritischer Bericht. Die Einleitung führt in die Gattung der böhmischen Pastorellen ein, stellt die Eigenheiten einer böhmischen Barockorgel vor und gibt Hinweise zu Aufführung, Komponisten und Werken. Der Notentext ist historisch-kritisch aufbereitet und musizierpraktisch gesetzt; der Kritische Bericht beschreibt die Editionspraxis, führt Quellennachweise und Einzelanmerkungen.',
      'The volume is in three parts: introduction, music text and critical report. The introduction presents the genre of the Bohemian pastorella, describes the characteristics of a Bohemian Baroque organ and gives notes on performance, composers and works. The music text is prepared along historical-critical lines and set for practical playing; the critical report describes the editorial method, gives source references and individual notes.',
      'Vydání se člení na tři části: úvod, notový text a kritickou zprávu. Úvod uvádí do žánru české pastorely, představuje zvláštnosti české barokní varhany a podává pokyny k provedení, skladatelům a dílům. Notový text je zpracován historicko-kriticky a vysázen pro praktické hraní; kritická zpráva popisuje ediční praxi, uvádí prameny a jednotlivé poznámky.'
    ),
  ];
  const specs = [
    [F('Herausgeber','Editor','Editor'), 'Roy Heyne'],
    [F('Verlag','Publisher','Vydavatel'), primary.verlag || 'Klaus-Jürgen Kamprad'],
    [F('Erschienen','Published','Vydáno'), primary.jahr || '2023'],
    ['ISMN', primary.ismn || '979-0-50258-340-8'],
  ];
  return (
    <div>
      <PageHeroDark t={t} eyebrow={t('interlude.eyebrow')} title={t('interlude.title')}
        lead={t.f(F('Böhmische Pastorellen des 18. Jahrhunderts für Orgel.','Bohemian pastorellas of the 18th century for organ.','České pastorely 18. století pro varhany.'))}
        image={window.RH_IMG.nativity} caption={t.f({ de: 'Jessica Zappe · Krippenszene (Deckelgemälde)', en: 'Jessica Zappe · Nativity scene (lid painting)', cs: 'Jessica Zappe · Betémská scéna (malba na víku)' })} />
      <section style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: '88px var(--gutter) 110px',
        display: 'grid', gridTemplateColumns: '0.8fr 1.2fr', gap: 72, alignItems: 'start' }}>
        <div style={{ position: 'sticky', top: 96, alignSelf: 'start' }}>
          <ImagePlate src={ednSlot ? ednSlot.src : ednCover} slot="edition-cover" imgPosition={ednSlot ? ednSlot.imgPosition : undefined} zoom={ednSlot ? ednSlot.zoom : undefined} ratio="3 / 4" plate={`${t('common.plate')} ${window.RH_PLATE('edition')}`} caption={t.f({ de: 'Umschlag der Edition', en: 'Edition cover', cs: 'Obálka edice' })} captionLines={1} />
        </div>
        <div>
          {descParas.map((p, i) => React.createElement('div', { key: i, style: { marginBottom: i < descParas.length - 1 ? 18 : 0 } },
            window.RH_ParityText({ as: 'p', lang: t.lang, all: p, className: i === 0 ? 'rh-dropcap' : undefined, textStyle: { fontSize: 'var(--fs-20)', lineHeight: 1.8, color: 'var(--text-body)', textAlign: 'justify', hyphens: 'auto', WebkitHyphens: 'auto' } })))}
          <div className="rh-num" style={{ borderTop: '1px solid var(--border-hairline)', marginTop: 28 }}>
            {specs.map(([lab, val], i) => (
              <div key={i} style={{ display: 'grid', gridTemplateColumns: '0.8fr 1.2fr', gap: 24, padding: '14px 0', borderBottom: '1px solid var(--border-hairline)' }}>
                <div style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--text-muted)' }}>{t.f(lab)}</div>
                <div style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-18)', color: 'var(--text-strong)' }}>{t.f(val)}</div>
              </div>
            ))}
          </div>
          <div style={{ display: 'flex', gap: 12, marginTop: 26, flexWrap: 'wrap' }}>
            <Badge tone="accent">{'17\u00A0' + (t.lang === 'en' ? 'pieces' : t.lang === 'cs' ? 'skladeb' : 'Stücke')}</Badge>
            <Badge>Brixi · Linek · Hladky · Ryba</Badge>
          </div>
          {/* Beide Knöpfe stehen auf EINER Grundlinie; die Quelle der Rezension
              hängt als Kapitälchen unter dem ganzen Block, nicht unter dem Knopf —
              sonst hebt sie den Knopf gegen den Nachbarn an. */}
          <div style={{ marginTop: 30, display: 'grid', gap: 8 }}>
            <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', alignItems: 'center' }}>
              <Button variant="primary" onClick={() => window.open((primary && primary.bestellen) || 'https://vkjk.de/noten/praeludia-pastoralis-pro-organo.html', '_blank', 'noopener')}
                iconAfter={<span>↗</span>}>{t.lang === 'en' ? 'Order from the publisher' : t.lang === 'cs' ? 'Objednat u vydavatele' : 'Beim Verlag bestellen'}</Button>
              {/* Der Rezensions-Knopf steht IMMER — er gehört zur Gestalt der Seite.
                  Ohne eingetragenen Link führt er zur Ausgabe beim Verlag (dort
                  stehen die Besprechungen); sobald Roy im Portal einen Link setzt,
                  führt er genau dorthin und die Quelle steht darunter. */}
              <Button variant="outline" onClick={() => window.open(_rezension || (primary && primary.bestellen) || 'https://vkjk.de/noten/praeludia-pastoralis-pro-organo.html', '_blank', 'noopener')}
                iconAfter={<span>↗</span>}>{t.f({ de: 'Rezension lesen', en: 'Read the review', cs: 'Přečíst recenzi' })}</Button>
              {!_rezension && _angemeldet && (
                <a href="/verwaltung/index.html" style={{ display: 'inline-flex', alignItems: 'center', gap: 8, minHeight: 44, padding: '0 14px', border: '1px dashed var(--messing)', borderRadius: 'var(--radius-sm)', color: 'var(--messing-dunkel)', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', textDecoration: 'none' }}>
                  ✎ {t.f({ de: 'Rezension verlinken', en: 'Link a review', cs: 'Připojit recenzi' })}
                </a>
              )}
            </div>
          </div>
          {editions.length > 1 && (
            <div style={{ marginTop: 40, paddingTop: 26, borderTop: '1px solid var(--border-hairline)' }}>
              <div className="rh-label" style={{ color: 'var(--messing-dunkel)', marginBottom: 16 }}>{t.f({ de: 'Weitere Ausgaben', en: 'Further editions', cs: 'Další vydání' })}</div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
                {editions.slice(1).map((e, i) => (
                  <div key={e.id != null ? e.id : i} style={{ display: 'grid', gridTemplateColumns: '64px 1fr', gap: 16, alignItems: 'center' }}>
                    <ImagePlate src={e.cover || (window.RH_IMG && window.RH_IMG.editionCover)} ratio="3 / 4" />
                    <div>
                      <div style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'var(--fs-18)', color: 'var(--text-strong)', lineHeight: 1.2 }}>{e.titel}</div>
                      <div style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)', marginTop: 3 }}>{[e.verlag, e.jahr, e.ismn].filter(Boolean).join(' · ')}</div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>
      </section>
    </div>
  );
}

/* ---------------- Instruments ---------------- */
// Apple-artige Instrument-Galerie: EIN großes Bogen-Tafelbild + ein manuell
//   antippbarer Miniaturen-Streifen (keine Automatik, kein Karussell) + Großansicht
//   (Lightbox) per Klick. Der Streifen blendet beim Scrollen gestaffelt ein; die
//   Großansicht trägt Instrument, Ansicht und Zähler. Ganz im Marken-Bildtreatment.
function useReveal() {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    // Sicherheitsnetz: nach kurzer Zeit IMMER einblenden — essenzielle Bedienelemente
    //   dürfen nie dauerhaft unsichtbar bleiben, falls der Observer nicht feuert.
    const fallback = setTimeout(() => setShown(true), 420);
    if (el && typeof IntersectionObserver !== 'undefined') {
      const io = new IntersectionObserver((ents) => { ents.forEach((e) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }); }, { threshold: 0.16 });
      io.observe(el);
      return () => { clearTimeout(fallback); io.disconnect(); };
    }
    setShown(true);
    return () => clearTimeout(fallback);
  }, []);
  return [ref, shown];
}
function InstrumentFigure({ it, t, flip }) {
  const { ImagePlate } = cpNS;
  const gallery = (it.gallery && it.gallery.length) ? it.gallery : [{ src: it.img, cap: it.name }];
  const [active, setActive] = React.useState(0);
  const [zoom, setZoom] = React.useState(false);
  const [lbErr, setLbErr] = React.useState(false);
  const [mag, setMag] = React.useState(1);
  const [pan, setPan] = React.useState({ x: 0, y: 0 });
  const [grab, setGrab] = React.useState(false);
  const [closing, setClosing] = React.useState(false);
  const [revealRef, shown] = useReveal();
  const idx = Math.min(active, gallery.length - 1);
  const g = gallery[idx] || gallery[0];
  const gSlot = (idx === 0 && it.slot && window.RH_slot) ? window.RH_slot(it.slot, { src: g.src }) : null;
  const many = gallery.length > 1;
  const go = (n) => setActive((n + gallery.length) % gallery.length);
  const open = () => { setLbErr(false); setZoom(true); };
  const pickZoom = (k) => { setActive(k); setLbErr(false); setZoom(true); };
  const closeLb = () => { setClosing(true); setTimeout(() => { setZoom(false); setClosing(false); }, 220); };
  const lbBtn = { width: 44, height: 44, borderRadius: '50%', display: 'grid', placeItems: 'center', cursor: 'pointer', background: 'rgba(20,18,14,0.6)', border: '1px solid rgba(246,239,217,0.35)', color: 'var(--pergament)', fontSize: 'var(--fs-20)', lineHeight: 1, backdropFilter: 'blur(3px)' };
  const magUp = () => setMag((m) => Math.min(4, +(m + 0.4).toFixed(2)));
  const magDown = () => setMag((m) => { const n = Math.max(1, +(m - 0.4).toFixed(2)); if (n === 1) setPan({ x: 0, y: 0 }); return n; });
  // Beim Öffnen und Bildwechsel die Lupe zurücksetzen.
  React.useEffect(() => { setMag(1); setPan({ x: 0, y: 0 }); }, [zoom, idx]);
  // Lightbox: Tastatur (Pfeile/ESC/+/-) + Body-Scroll sperren.
  React.useEffect(() => {
    if (!zoom) return undefined;
    const onKey = (e) => {
      if (e.key === 'Escape') closeLb();
      else if (e.key === 'ArrowRight' && many) go(idx + 1);
      else if (e.key === 'ArrowLeft' && many) go(idx - 1);
      else if (e.key === '+' || e.key === '=') magUp();
      else if (e.key === '-' || e.key === '_') magDown();
    };
    document.addEventListener('keydown', onKey);
    const prev = document.body.style.overflow; document.body.style.overflow = 'hidden';
    return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = prev; };
  }, [zoom, idx, many]);
  const onImgDown = (e) => {
    if (mag <= 1) return;
    e.preventDefault(); e.stopPropagation();
    const sx = e.clientX, sy = e.clientY, base = { ...pan };
    setGrab(true);
    const move = (ev) => setPan({ x: base.x + (ev.clientX - sx), y: base.y + (ev.clientY - sy) });
    const up = () => { setGrab(false); window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); };
    window.addEventListener('pointermove', move); window.addEventListener('pointerup', up);
  };
  const onWheelImg = (e) => { e.preventDefault(); e.stopPropagation(); setMag((m) => { const n = Math.max(1, Math.min(4, +(m - e.deltaY * 0.0022).toFixed(3))); if (n === 1) setPan({ x: 0, y: 0 }); return n; }); };
  const onDblImg = (e) => { e.stopPropagation(); setMag((m) => (m > 1 ? 1 : 2.4)); setPan({ x: 0, y: 0 }); };
  const chev = (dir) => (
    <button type="button" aria-label={dir < 0 ? t.f({ de: 'Vorheriges Bild', en: 'Previous image', cs: 'Předchozí obraz' }) : t.f({ de: 'Nächstes Bild', en: 'Next image', cs: 'Další obraz' })}
      onClick={(e) => { e.stopPropagation(); go(idx + dir); }}
      style={{ position: 'absolute', top: '50%', [dir < 0 ? 'left' : 'right']: 'clamp(12px, 4vw, 48px)', transform: 'translateY(-50%)', width: 52, height: 52, borderRadius: '50%', display: 'grid', placeItems: 'center', cursor: 'pointer', background: 'rgba(20,18,14,0.5)', border: '1px solid rgba(246,239,217,0.35)', color: 'var(--pergament)', fontFamily: 'var(--font-display)', fontSize: 'var(--fs-26)', lineHeight: 1 }}>
      {dir < 0 ? '‹' : '›'}
    </button>
  );
  return (
    <div ref={revealRef} style={{ order: flip ? 2 : 1 }}>
      {/* Großes Tafelbild — anklickbar zum Vergrößern (Apple-Galerie) */}
      <div key={idx} className="rh-plate-fade" style={{ position: 'relative', cursor: 'zoom-in' }}
        onClick={open} role="button" tabIndex={0}
        aria-label={t.f({ de: 'Bild vergrößern', en: 'Enlarge image', cs: 'Zvětšit obraz' })}
        onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }}>
        <ImagePlate src={gSlot ? gSlot.src : g.src} slot={idx === 0 ? it.slot : undefined} imgPosition={gSlot ? gSlot.imgPosition : undefined} zoom={gSlot ? gSlot.zoom : undefined} ratio="3 / 2" plate={it.plate ? `${t('common.plate')} ${it.plate}` : undefined} caption={t.f(g.cap || it.name)} />
        <span aria-hidden="true" style={{ position: 'absolute', top: 18, right: 18, width: 40, height: 40, borderRadius: '50%', display: 'grid', placeItems: 'center', background: 'rgba(20,18,14,0.42)', backdropFilter: 'blur(4px)', border: '1px solid rgba(246,239,217,0.4)', color: 'var(--pergament)', fontSize: 'var(--fs-17)', pointerEvents: 'none' }}>⤢</span>
      </div>
      {many && (
        <React.Fragment>
          <div className="rh-label" style={{ color: 'var(--messing-dunkel)', margin: '18px 0 10px', opacity: shown ? 1 : 0, transform: shown ? 'none' : 'translateY(6px)', transition: 'opacity 520ms var(--ease-out), transform 520ms var(--ease-out)' }}>
            {t.f({ de: 'Ansicht antippen zum Vergrößern', en: 'Tap a view to enlarge', cs: 'Klepnutím pohled zvětšíte' })}
          </div>
          <div role="group" aria-label={t.f({ de: 'Weitere Ansichten', en: 'More views', cs: 'Další pohledy' })}
            style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
            {gallery.map((im, k) => (
              <button key={k} type="button" onClick={() => pickZoom(k)} aria-current={k === idx}
                aria-label={t.f({ de: 'Vergrößern', en: 'Enlarge', cs: 'Zvětšit' }) + ': ' + t.f(im.cap || it.name)} title={t.f(im.cap || it.name)}
                style={{ position: 'relative', width: 96, height: 64, padding: 0, borderRadius: 'var(--radius-sm)', overflow: 'hidden', cursor: 'zoom-in', background: 'var(--nachtschiefer)', border: 'none', boxShadow: k === idx ? '0 0 0 2px var(--messing), 0 4px 12px rgba(20,18,14,0.16)' : 'inset 0 0 0 1px var(--border-hairline)', opacity: shown ? (k === idx ? 1 : 0.62) : 0, transform: shown ? (k === idx ? 'translateY(-2px)' : 'none') : 'translateY(10px)', transition: 'opacity 480ms var(--ease-out) ' + (60 + k * 70) + 'ms, transform 480ms var(--ease-out) ' + (60 + k * 70) + 'ms, box-shadow var(--dur-fast) var(--ease-standard)' }}>
                <img loading="lazy" decoding="async" src={im.src} alt="" aria-hidden="true" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
              </button>
            ))}
          </div>
        </React.Fragment>
      )}
      {/* Lightbox — Großansicht jedes Bildes mit Instrument, Ansicht & Zähler */}
      {zoom && ReactDOM.createPortal((
        <div onClick={closeLb} role="dialog" aria-modal="true" aria-label={t.f(it.name)}
          style={{ position: 'fixed', inset: 0, zIndex: 1200, background: 'rgba(18,16,12,0.94)', backdropFilter: 'blur(6px)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 'clamp(20px, 5vw, 64px)', animation: (closing ? 'rh-lb-overlay-out' : 'rh-lb-overlay-in') + ' 220ms var(--ease-out) both' }}>
          <button type="button" aria-label={t.f({ de: 'Schließen', en: 'Close', cs: 'Zavřít' })} onClick={(e) => { e.stopPropagation(); closeLb(); }}
            style={{ position: 'absolute', top: 'clamp(14px, 3vw, 30px)', right: 'clamp(14px, 3vw, 30px)', width: 46, height: 46, borderRadius: '50%', display: 'grid', placeItems: 'center', cursor: 'pointer', background: 'rgba(20,18,14,0.5)', border: '1px solid rgba(246,239,217,0.3)', color: 'var(--pergament)', fontSize: 'var(--fs-22)', lineHeight: 1 }}>✕</button>
          <figure onClick={(e) => e.stopPropagation()} style={{ margin: 0, maxWidth: 'min(92vw, 1100px)', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 18, animation: (closing ? 'rh-lb-fig-out' : 'rh-lb-fig-in') + ' 240ms var(--ease-out) both' }}>
            {lbErr
              ? <div style={{ width: 'min(80vw, 660px)', aspectRatio: '3 / 2', display: 'grid', placeItems: 'center', gap: 12, border: '1px solid rgba(246,239,217,0.18)', background: 'rgba(246,239,217,0.04)', color: 'var(--pergament)', textAlign: 'center', padding: 24 }}>
                  <span aria-hidden="true" style={{ width: 1, height: 28, background: 'var(--messing)', opacity: 0.7 }} />
                  <span className="rh-label" style={{ letterSpacing: '0.2em', color: 'rgba(246,239,217,0.72)' }}>{t.f({ de: 'Abbildung folgt', en: 'Image to follow', cs: 'Obrázek bude doplněn' })}</span>
                </div>
              : <div onWheel={onWheelImg} style={{ position: 'relative', maxWidth: 'min(92vw, 1100px)', maxHeight: '74vh', overflow: 'hidden', border: '1px solid rgba(246,239,217,0.18)', boxShadow: '0 30px 80px rgba(0,0,0,0.5)', background: 'var(--plate-void-dark)', borderRadius: 2 }}>
                  <img loading="lazy" decoding="async" key={idx} src={g.src} alt={t.f(g.cap || it.name)} draggable={false} onError={() => setLbErr(true)} onPointerDown={onImgDown} onDoubleClick={onDblImg}
                    style={{ display: 'block', maxWidth: '100%', maxHeight: '74vh', objectFit: 'contain', transform: 'translate(' + pan.x + 'px, ' + pan.y + 'px) scale(' + mag + ')', transformOrigin: 'center', transition: grab ? 'none' : 'transform 220ms var(--ease-out)', cursor: mag > 1 ? (grab ? 'grabbing' : 'grab') : 'zoom-in', userSelect: 'none', touchAction: 'none' }} />
                  <div style={{ position: 'absolute', right: 12, bottom: 12, display: 'flex', gap: 6 }}>
                    <button type="button" aria-label={t.f({ de: 'Verkleinern', en: 'Zoom out', cs: 'Zmenšit' })} onClick={(e) => { e.stopPropagation(); magDown(); }} style={lbBtn}>−</button>
                    <button type="button" aria-label={t.f({ de: 'Vergrößern', en: 'Zoom in', cs: 'Zvětšit' })} onClick={(e) => { e.stopPropagation(); magUp(); }} style={lbBtn}>＋</button>
                  </div>
                  {mag > 1 && <div style={{ position: 'absolute', left: 12, bottom: 12, padding: '4px 10px', borderRadius: 999, background: 'rgba(20,18,14,0.6)', backdropFilter: 'blur(3px)', color: 'var(--pergament)', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12)', letterSpacing: '.04em', pointerEvents: 'none', display: 'flex', gap: 6, alignItems: 'center' }}><span aria-hidden="true">✥</span>{t.f({ de: 'Ziehen für Details', en: 'Drag to explore', cs: 'Táhněte pro detail' })}</div>}
                </div>}
            <figcaption style={{ textAlign: 'center', display: 'grid', gap: 7, justifyItems: 'center' }}>
              <span className="rh-label" style={{ color: 'var(--messing)', letterSpacing: '0.2em' }}>{t.f(it.name)}{it.plate ? ' · ' + t('common.plate') + ' ' + it.plate : ''}</span>
              <span style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontSize: 'clamp(1.05rem, 0.9rem + 0.7vw, 1.45rem)', color: 'var(--pergament)', lineHeight: 1.25, textWrap: 'balance' }}>{t.f(g.cap || it.name)}</span>
              <span aria-hidden="true" style={{ width: 34, height: 1, background: 'rgba(246,239,217,0.28)', margin: '3px 0' }} />
              {many ? (
                <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 2 }}>
                  <button type="button" aria-label={t.f({ de: 'Vorheriges Bild', en: 'Previous image', cs: 'Předchozí obraz' })} onClick={(e) => { e.stopPropagation(); go(idx - 1); }}
                    style={{ width: 44, height: 44, display: 'grid', placeItems: 'center', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--pergament)', fontFamily: 'var(--font-display)', fontSize: 'var(--fs-26)', lineHeight: 1, opacity: 0.85 }}>‹</button>
                  <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', letterSpacing: '0.14em', color: 'rgba(246,239,217,0.72)', minWidth: 62, textAlign: 'center' }}>{(idx + 1) + ' / ' + gallery.length}</span>
                  <button type="button" aria-label={t.f({ de: 'Nächstes Bild', en: 'Next image', cs: 'Další obraz' })} onClick={(e) => { e.stopPropagation(); go(idx + 1); }}
                    style={{ width: 44, height: 44, display: 'grid', placeItems: 'center', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--pergament)', fontFamily: 'var(--font-display)', fontSize: 'var(--fs-26)', lineHeight: 1, opacity: 0.85 }}>›</button>
                </div>
              ) : <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', letterSpacing: '0.14em', color: 'rgba(246,239,217,0.72)' }}>{t.f({ de: 'Aus Roys Archiv', en: 'From Roy’s archive', cs: 'Z Royova archivu' })}</span>}
            </figcaption>
          </figure>
        </div>
      ), document.body)}
    </div>
  );
}

// Hochgestellte Töne in Dispositionen: „F^D“ heißt — die F-Taste klingt als D
// (kurze Bassoktave). Als Zeichenkette geschrieben, hier zu echtem <sup> gesetzt;
// verlässliche Unicode-Hochstellungen für D und E gibt es in den Hausschriften
// nicht. Der Klammerzusatz rückt kleiner und ruhiger, damit der Umfang die
// Zeile führt und der Hinweis nicht mit ihm um Aufmerksamkeit streitet.
function _hoch(txt) {
  const s = String(txt == null ? '' : txt);
  const teile = s.split(/(\^[A-Ha-h]|\([^)]*\))/g).filter(Boolean);
  return teile.map((x, i) => {
    if (/^\^/.test(x)) return React.createElement('sup', { key: i, style: { fontSize: '0.72em', lineHeight: 0, verticalAlign: 'super', letterSpacing: 0 } }, x.slice(1));
    if (/^\(/.test(x)) return React.createElement('span', { key: i, style: { fontSize: '0.86em', color: 'var(--text-muted)', letterSpacing: '0.02em', whiteSpace: 'nowrap' } }, x);
    return x;
  });
}

function InstrumentsPage({ onNav, t }) {
  const { Ornament } = cpNS;
  const RICH = [
    {
      img: window.RH_IMG.cembaloFull, id: 'cembalo', slot: 'cembalo-full', plate: window.RH_PLATE('cembalo'),
      name: F('Deutsches Cembalo nach Christian Vater','German harpsichord after Christian Vater','Německé cembalo podle Christiana Vatera'),
      meta: F('nach Vater (Hannover, 1738) · Erbauer Christian Rothe (Berlin, 2018)','after Vater (Hanover, 1738) · built by Christian Rothe (Berlin, 2018)','podle Vatera (Hannover, 1738) · stavitel Christian Rothe (Berlín, 2018)'),
      text: F('Das Deckelgemälde von Jessica Zappe hat die Geburt Christi zum Thema: Im Zentrum ruht das Kind im Licht, um das sich alles ordnet — die anbetenden Hirten, die Heiligen Drei Könige und der „Gloria in excelsis Deo“-Engel. Eine barocke Nachtszene im Helldunkel, von Goldornamenten gerahmt.','Jessica Zappe’s lid painting takes the Nativity as its subject: at the centre the Christ child rests in light, and everything orders itself around it — the adoring shepherds, the Three Kings and the “Gloria in excelsis Deo” angel. A Baroque nocturne in chiaroscuro, framed by gilt ornament.','Malba na víku od Jessiky Zappe má za téma Narození Krista: uprostřed spočívá dítě ve světle, k němuž se vše vztahuje — klanící se pasýři, Tři králové a anděl „Gloria in excelsis Deo“. Barokní noční scéna v šerosvitu, rámovaná zlatým ornamentem.'),
      // Disposition wie beim Orgelpositiv: Umfang und Register in derselben
      // Schreibweise (Fußtonzahlen mit dem typografischen Minutenzeichen ′).
      disp: { head: F('Disposition: G₁, C – e′′′', 'Disposition: G₁, C – e′′′', 'Dispozice: G₁, C – e′′′'), stops: [['CHOR I', '8′'], ['CHOR II', '8′']] },
      gallery: [
        { src: window.RH_IMG.cembaloFull, cap: F('Das ganze Instrument','The whole instrument','Celý nástroj') },
        { src: window.RH_IMG.cembaloGilt, cap: F('Goldornamente · Deckel','Gilt ornaments · lid','Zlacené ornamenty · víko') },
        { src: window.RH_IMG.cembaloKeys, cap: F('Cembalo nach Christian Vater, 1738 · die Tastatur aus der Nähe','Harpsichord after Christian Vater, 1738 · the keys up close','Cembalo podle Christiana Vatera, 1738 · klávesy zblízka') },
        { src: window.RH_IMG.kings, cap: F('Deckelgemälde · Die Heiligen Drei Könige (Jessica Zappe)','Lid painting · The Three Kings (Jessica Zappe)','Malba na víku · Tři králové (Jessica Zappe)') },
        { src: window.RH_IMG.gloria, cap: F('Detail des Deckelgemäldes · Gloria-Engel','Lid painting detail · Gloria angel','Detail malby na víku · anděl Gloria') },
        { src: window.RH_IMG.nativity, cap: F('Deckelgemälde · Die Krippe','Lid painting · The Nativity','Malba na víku · Betlém') },
        { src: window.RH_IMG.duoRoom, cap: F('Cembalo und Orgelpositiv im Wohnzimmer','Harpsichord and positive organ in the living room','Cembalo a varhanní pozitiv v obývacím pokoji') },
      ],
    },
    {
      img: window.RH_IMG.ottavinoTop, id: 'ottavino', slot: 'ottavino-top', plate: window.RH_PLATE('ottavino'),
      name: F('Ottavino (Oktavspinett)','Ottavino (octave spinet)','Ottavino (oktávový spinet)'),
      meta: F('nach anonymem italienischem Vorbild, 17. Jh. · Erbauer Colin Booth (Wells, GB, 2018)','after an anonymous Italian model, 17th c. · built by Colin Booth (Wells, UK, 2018)','podle anonymní italské předlohy, 17. stol. · stavitel Colin Booth (Wells, GB, 2018)'),
      // Kurze Bassoktave: die Tasten F und G klingen als D und E — im 17. Jahrhundert
      // war Platz im Bass teurer als Vollständigkeit.
      disp: { head: F('Disposition: C  F^D  G^E  A – c′′′ (kurze Bassoktave)', 'Disposition: C  F^D  G^E  A – c′′′ (short bass octave)', 'Dispozice: C  F^D  G^E  A – c′′′ (krátká basová oktáva)'), stops: [['CHOR', '4′']] },
      text: F('Das Ottavino ist ein kleines Oktavspinett — es klingt eine Oktave höher als ein Cembalo. Gerade seine kleine Größe machte es seit dem 16. Jahrhundert praktisch: leicht zu tragen, rasch aufgestellt, ideal für die Hausmusik und als tragbares Continuo-Instrument im Ensemble. Sein heller, perlender Klang trägt auch in kleinen Räumen und bei intimen Programmen.','The ottavino is a small octave spinet — it sounds an octave above a harpsichord. Its very smallness made it practical from the 16th century on: easy to carry, quick to set up, ideal for domestic music-making and as a portable continuo instrument in an ensemble. Its bright, pearling sound carries even in small rooms and intimate programmes.','Ottavino je malý oktávový spinet — zní o oktávu výše než cembalo. Právě jeho malé rozměry jej od 16. století činily praktickým: snadno se přenáší, rychle postaví, ideální pro domácí muzicírování a jako přenosný continuo nástroj v souboru. Jeho jasný, perlivý zvuk nese i v malých prostorách a komorních programech.'),
      gallery: [
        { src: window.RH_IMG.ottavinoTop, cap: F('Von oben','From above','Shora') },
        { src: window.RH_IMG.ottavinoName, cap: F('Namenszug','Nameboard','Jmenovka') },
        { src: window.RH_IMG.ottavinoSide, cap: F('Colin Booth hat es gefertigt 2018','Colin Booth made this in 2018','Colin Booth to zhotovil 2018') },
      ],
    },
    {
      img: window.RH_IMG.positivFront, id: 'positiv', slot: 'positiv-front', plate: window.RH_PLATE('positiv'),
      name: F('Orgelpositiv','Positive organ','Varhanní pozitiv'),
      meta: F('nach böhmischem Vorbild, 18. Jh. · Erbauer Dlabal-Mettler (Bílsko bei Olmütz, 2021)','after a Bohemian model, 18th c. · built by Dlabal-Mettler (Bílsko near Olomouc, 2021)','podle českého vzoru, 18. stol. · stavitel Dlabal-Mettler (Bílsko u Olomouce, 2021)'),
      text: F('Stimmung gemildert mitteltönig, a′ = 415 Hz.\nIn Marmoroptik von musizierenden Engeln bekrönt.','Tempered meantone, a′ = 415 Hz.\nCrowned with music-making angels in a marble finish.','Zmírněný středotón, a′ = 415 Hz.\nKorunováno hudoucími anděly v mramorovém provedení.'),
      disp: { head: F('Disposition: C, D – c′′′','Disposition: C, D – c′′′','Dispozice: C, D – c′′′'), stops: [['COPVLA major', '8′'], ['COPVLA minor', '4′'], ['PRINCIPAL', '2′'], ['OCTAV', '1′']] },
      gallery: [
        { src: window.RH_IMG.positivFront, cap: F('Prospekt mit musizierenden Engeln','Case with music-making angels','Skříň s hudoucími anděly') },
        { src: window.RH_IMG.positivPipes, cap: F('Prospekt-Ansicht','View of the case front','Pohled na prospekt') },
        { src: window.RH_IMG.organStops, cap: F('Registerzüge · Copula major & minor, Principal, Octav','Stop knobs · Copula major & minor, Principal, Octav','Rejstříková manubria · Copula major & minor, Principal, Octav') },
        { src: window.RH_IMG.positivKeys, cap: F('Klaviatur mit verziertem Notenpult','Keyboard with ornate music desk','Klaviatura se zdobeným notovým pultem') },
        { src: window.RH_IMG.angelCrown, cap: F('Paukenengel','Timpani angel','Anděl s tympány') },
        { src: window.RH_IMG.positivTrumpet, cap: F('Trompeten-Engel im Profil','Trumpeting angel in profile','Trubící anděl z profilu') },
        { src: window.RH_IMG.positivDesk, cap: F('Ansicht von der Seite','Side view','Pohled ze strany') },
        { src: window.RH_IMG.positivCorner, cap: F('Eckdetail · Paukenengel und vergoldetes Schnitzwerk','Corner detail · timpani angel and gilt carving','Rohový detail · anděl s tympány a zlacené řezby') },
      ],
    },
  ];
  // Datengetrieben: im Portal (Orte & Instrumente) gepflegte Instrumente. Kuratierte
  //   Haupt-Instrumente behalten ihren reichen Text; Roys Feld-Änderungen (Erbauer/
  //   Jahr/Stimmung) überschreiben die Meta-Zeile, neu angelegte erscheinen als Karten.
  const _norm = (s) => (s || '').toLowerCase().replace(/[^a-zäöü0-9]/g, '');
  const stored = window.RH_entities ? (window.RH_entities('instrumente', null) || []) : [];
  stored.forEach((row) => {
    const idx = RICH.findIndex((r) => _norm(r.name.de).slice(0, 14) === _norm(row.name).slice(0, 14));
    if (idx >= 0) { const m = [row.erbauer, row.jahr, row.stimmung].filter(Boolean).join(' · '); if (m) RICH[idx] = Object.assign({}, RICH[idx], { meta: F(m, m, m) }); }
  });
  const extra = stored
    .filter((row) => !RICH.some((r) => _norm(r.name.de).slice(0, 14) === _norm(row.name).slice(0, 14)) && !/ranft/.test(_norm(row.name)))
    .map((row) => ({ img: (window.RH_IMG && window.RH_IMG.positivRoom), id: null, plate: null, name: F(row.name, row.name, row.name), meta: F([row.erbauer, row.jahr, row.stimmung].filter(Boolean).join(' · ')), text: F('', '', '') }));
  const items = [...RICH, ...extra];
  return (
    <div>
      <PageHeroDark t={t} eyebrow={t('teaser.instr_label')} title={t('teaser.instr_title')}
        lead={t('teaser.instr_text')} image={window.RH_IMG.positivRoom} bgImage={window.RH_IMG.pastoralLand} slot="instr-hero" caption={t.f({ de: 'Im Wohnzimmer · Cembalo und Orgelpositiv', en: 'In the living room · harpsichord and positive organ', cs: 'V obývacím pokoji · cembalo a varhanní pozitiv' })} imgPos="center 45%" />
      <section style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: '88px var(--gutter) 110px' }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 88 }}>
          {items.map((it, i) => (
            <React.Fragment key={i}>
              <div className="rh-instr-row" style={{ display: 'grid', gridTemplateColumns: i % 2 ? '0.85fr 1.15fr' : '1.15fr 0.85fr', gap: 60, alignItems: 'start' }}>
                <InstrumentFigure it={it} t={t} flip={!!(i % 2)} />
                <div style={{ order: i % 2 ? 1 : 2 }}>
                  <div className="rh-eyebrow" style={{ marginBottom: 16 }}>
                    <span style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', textTransform: 'none', letterSpacing: 0, fontSize: 'var(--fs-17)', marginRight: 2 }}>{it.plate}.</span>
                    {t('teaser.instr_label')}
                  </div>
                  <h2 style={{ fontSize: 'clamp(1.8rem, 1.3rem + 1.3vw, 2.4rem)', margin: '0 0 10px' }}>{window.RH_pt(t.f(it.name))}</h2>
                  <div className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--messing-dunkel)', marginBottom: 16, letterSpacing: '0.01em', whiteSpace: 'pre-line', lineHeight: 1.6 }}>{t.f(it.meta).replace(' · ', '\n')}</div>
                  {/* Blocksatz nur für DURCHLAUFENDE Prosa. Trägt der Text einen
                      gesetzten Umbruch, wird die Zeile davor künstlich gestreckt
                      („In   Marmoroptik   von   musizierenden“) — dort ist
                      linksbündig das Richtige. `lang` je Sprache für die
                      Silbentrennung, `pre-line` hält den Umbruch. */}
                  <p lang={t.lang} style={{ fontSize: 'var(--fs-19)', lineHeight: 1.75, color: 'var(--text-body)', margin: 0, textAlign: String(t.f(it.text)).indexOf('\n') === -1 ? 'justify' : 'left', hyphens: 'auto', WebkitHyphens: 'auto', whiteSpace: 'pre-line' }}>{t.f(it.text)}</p>
                  {it.disp && (
                    <div style={{ marginTop: 20 }}>
                      <div className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--messing-dunkel)', letterSpacing: '0.04em', marginBottom: 10 }}>{_hoch(t.f(it.disp.head))}</div>
                      {it.disp.stops && it.disp.stops.length > 0 && (
                      <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
                        {it.disp.stops.map((s, k) => (
                          <li key={k} style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-17-5)', color: 'var(--text-strong)', letterSpacing: '0.02em' }}>{s[0]} {s[1]}</li>
                        ))}
                      </ul>
                      )}
                    </div>
                  )}
                </div>
              </div>
              {i < items.length - 1 && <Ornament mark={['leaf','heart','asterism'][i % 3]} />}
            </React.Fragment>
          ))}
        </div>
      </section>
    </div>
  );
}

/* ---------------- Graceful placeholder ---------------- */
function SoonPage({ route, onNav, t }) {
  const { Button } = cpNS;
  return (
    <section style={{ maxWidth: '640px', margin: '0 auto', padding: '120px var(--gutter) 160px', minHeight: '52vh', textAlign: 'center' }}>
      <span aria-hidden="true" style={{ color: 'var(--messing)', fontSize: 'var(--fs-22)', display: 'inline-block', marginBottom: 22 }}>❧</span>
      <div className="rh-label" style={{ color: 'var(--messing-dunkel)', marginBottom: 14 }}>{t('soon.label')}</div>
      <h1 style={{ fontSize: 'clamp(2.2rem, 1.7rem + 2vw, 3.2rem)', margin: '0 0 16px' }}>{window.RH_pt(t('soon.title'))}</h1>
      <p style={{ fontSize: 'var(--fs-19)', lineHeight: 1.7, color: 'var(--text-body)', maxWidth: '46ch', margin: '0 auto 30px' }}>{t('soon.text')}</p>
      <Button variant="outline" onClick={() => onNav('start')}>{t('soon.cta')}</Button>
    </section>
  );
}

/* ---------------- Impressum & Datenschutz ---------------- */
const LEGAL = {
  impressum: {
    eyebrow: 'Rechtliches',
    title: 'Impressum',
    intro: 'Angaben gemäß § 5 DDG und § 18 Abs. 2 MStV',
    sections: [
      { h: 'Diensteanbieter', body: window.RH_STAMM.anschriftLand },
      { h: 'Kontakt', body: ['Telefon: ' + window.RH_STAMM.telefon, 'E-Mail: ' + window.RH_STAMM.email] },
      { h: 'Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV', body: [window.RH_STAMM.name, window.RH_STAMM.strasse, window.RH_STAMM.ort, window.RH_STAMM.land] },
      { h: 'Bildnachweise', body: ['Cembalo-Deckelgemälde: Jessica Zappe', 'Fotografien: Daniel Förster', 'Signet Geisinger Orgelsommer: Stich der Ranft-Orgel; Nutzung und Bearbeitung mit entsprechender Berechtigung', 'Historische Gemäldeausschnitte: gemeinfreie Werke. Nähere Angaben zu Künstler, Werk, Quelle, Lizenzstatus und etwaigen Bearbeitungen erfolgen, soweit erforderlich, unmittelbar am jeweiligen Bild oder in den ergänzenden Bildnachweisen dieser Website.'] },
      { h: 'Haftung für Inhalte', body: ['Als Diensteanbieter bin ich gemäß § 7 Abs. 1 DDG für eigene Inhalte auf dieser Website nach den allgemeinen Gesetzen verantwortlich. Eine Verpflichtung zur Überwachung übermittelter oder gespeicherter fremder Informationen besteht nur nach Maßgabe der gesetzlichen Vorschriften.'] },
      { h: 'Haftung für Links', body: ['Diese Website enthält Links zu externen Websites Dritter. Auf deren Inhalte habe ich keinen Einfluss. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber verantwortlich. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Bei Bekanntwerden entsprechender Rechtsverletzungen werden solche Links entfernt.'] },
      { h: 'Urheberrecht', body: ['Die durch den Seitenbetreiber erstellten Inhalte und Werke auf dieser Website unterliegen dem deutschen Urheberrecht. Beiträge Dritter sind als solche gekennzeichnet. Jede Verwertung außerhalb der Grenzen des Urheberrechts bedarf der vorherigen Zustimmung des jeweiligen Rechteinhabers.'] },
      { h: 'Verbraucherstreitbeilegung', body: ['Ich bin weder verpflichtet noch bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.'] },
    ],
  },
  // Die Datenschutzerklärung lebt in templates/website/datenschutz.js — als Daten,
  // nicht als Prosa in der Komponente: der Jurist ändert dort, das Layout hier.
  datenschutz: {
    eyebrow: 'Rechtliches',
    title: 'Datenschutz',
    intro: 'Diese Datenschutzerklärung informiert darüber, wie auf dieser Website personenbezogene Daten verarbeitet werden. Sie beschreibt die tatsächliche technische Konfiguration — Bereitstellung über Cloudflare, Kontaktformular, Newsletter, Karten und Videos erst nach Ihrer Einwilligung — und Ihre Rechte nach der Datenschutz-Grundverordnung (DSGVO) und dem Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz (TDDDG).',
    sections: (window.RH_DATENSCHUTZ && window.RH_DATENSCHUTZ.sections) || [],
    stand: (window.RH_DATENSCHUTZ && window.RH_DATENSCHUTZ.stand) || '',
  },
};

function LegalPage({ doc, onNav, t }) {
  const { Ornament, Button } = cpNS;
  const d = LEGAL[doc] || LEGAL.impressum;
  const L = t && t.lang || 'de';
  // UI-Chrome dreisprachig; der Rechtstext selbst bleibt bewusst deutsch (verbindliche Fassung).
  const eyebrow = { de: 'Rechtliches', en: 'Legal', cs: 'Právní informace' }[L];
  const title = doc === 'impressum'
    ? { de: 'Impressum', en: 'Legal notice', cs: 'Impresum' }[L]
    : { de: 'Datenschutz', en: 'Privacy', cs: 'Ochrana údajů' }[L];
  const stand = d.stand
    ? { de: 'Stand: ' + d.stand, en: 'As of ' + d.stand, cs: 'Stav: ' + d.stand }[L]
    : { de: 'Stand: Juni 2026', en: 'As of June 2026', cs: 'Stav: červen 2026' }[L];
  const nextRoute = doc === 'impressum' ? 'datenschutz' : 'bildnachweise';
  const switchLabel = doc === 'impressum'
    ? { de: 'Zur Datenschutzerklärung', en: 'To the privacy policy', cs: 'K ochraně údajů' }[L]
    : { de: 'Zu den Bildnachweisen', en: 'To the image credits', cs: 'K obrazovým zdrojům' }[L];
  return (
    <div>
      <section style={{ position: 'relative', overflow: 'hidden', color: 'var(--pergament-text)', background: 'var(--scrim-1)' }}>
        <div style={{ position: 'relative', maxWidth: 'var(--container)', margin: '0 auto', padding: '120px var(--gutter) 56px' }}>
          <div className="rh-eyebrow rh-eyebrow--dark" style={{ marginBottom: 16 }}>{eyebrow}</div>
          <h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2.4rem, 1.8rem + 2.4vw, 3.8rem)', color: 'var(--pergament-text)', margin: 0 }}>{title}.</h1>
        </div>
      </section>
      <section className="rh-flat" style={{ background: 'var(--pergament)' }}>
        <div style={{ maxWidth: '64ch', margin: '0 auto', padding: '64px var(--gutter) 40px' }}>
          {window.RH_ParityText({
            as: 'p', lang: L,
            all: { de: '', en: 'For legal reasons the binding version is the German text below.', cs: 'Z právních důvodů je závazné německé znění.' },
            style: { marginBottom: 32 },
            styleFor: (l) => l === 'de' ? {} : { fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13-5)', color: 'var(--text-muted)', borderLeft: '2px solid var(--messing)', paddingLeft: 14 },
          })}
          <p className="rh-dropcap" style={{ fontSize: 'var(--fs-19)', lineHeight: 1.75, color: 'var(--text-body)', marginTop: 0, marginBottom: 8 }}>{d.intro}</p>
          {d.sections.map((s, i) => (
            <div key={i} style={{ marginTop: 38 }}>
              <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'clamp(1.4rem, 1.1rem + 0.8vw, 1.8rem)', margin: '0 0 12px', color: 'var(--text-strong)' }}>{s.h}.</h2>
              {s.body.map((line, j) => (
                <p key={j} style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-17-5)', lineHeight: 1.7, color: 'var(--text-body)', margin: '0 0 6px', maxWidth: '60ch' }}>{line}</p>
              ))}
            </div>
          ))}
          <Ornament mark="leaf" style={{ margin: '56px auto 40px' }} />
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 16 }}>
            <span style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)' }}>{stand}</span>
            <Button variant="outline" onClick={() => onNav(nextRoute)}>
              {switchLabel} →
            </Button>
          </div>
        </div>
      </section>
    </div>
  );
}

/* ---------------- Kontakt ---------------- */
function KontaktPage({ onNav, t }) {
  const { Button, TextField } = cpNS;
  const [sent, setSent] = React.useState(false);
  return (
    <div>
      <PageHeroDark t={t} eyebrow={t('contact.eyebrow')} title={t('contact.title')}
        lead={t('contact.lead')} image={window.RH_IMG.positivRoom} slot="kontakt-hero" caption={t.f({ de: 'Cembalo und Orgelpositiv · Dresden', en: 'Harpsichord and positive organ · Dresden', cs: 'Cembalo a varhanní pozitiv · Drážďany' })} imgPos="center 45%" />
      <section style={{ background: 'var(--pergament)' }}>
        <div style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: '80px var(--gutter) 110px',
          display: 'grid', gridTemplateColumns: '1.1fr 0.9fr', gap: 72, alignItems: 'start' }}>
          <div>
            {sent ? (
              <div style={{ padding: '40px 0' }}>
                <span aria-hidden="true" style={{ color: 'var(--messing)', fontSize: 'var(--fs-26)' }}>❧</span>
                <p style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontSize: 'clamp(1.6rem, 1.2rem + 1.4vw, 2.2rem)', color: 'var(--text-strong)', marginTop: 14 }}>{t('contact.sent')}</p>
              </div>
            ) : (
              <form onSubmit={(e) => { e.preventDefault(); setSent(true); }} style={{ display: 'flex', flexDirection: 'column', gap: 20, maxWidth: 460 }}>
                <TextField label={t('contact.name')} required />
                <TextField label={t('contact.email')} type="email" required />
                <TextField label={t('contact.subject')} />
                <label style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
                  <span style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', fontWeight: 500, color: 'var(--text-muted)', letterSpacing: '0.02em' }}>{t('contact.message')}</span>
                  <textarea rows={5} required style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-16)', padding: '12px 14px', borderRadius: 'var(--radius-sm)',
                    border: '1px solid var(--border-hairline)', background: 'var(--surface-card)', color: 'var(--text-strong)', resize: 'vertical' }} />
                </label>
                <div><Button type="submit">{t('contact.send')}</Button></div>
              </form>
            )}
          </div>
          <aside style={{ display: 'flex', flexDirection: 'column', gap: 28 }}>
            <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', padding: '24px 26px', background: 'var(--surface-card)' }}>
              <div className="rh-label" style={{ color: 'var(--messing-dunkel)', marginBottom: 14 }}>{t('contact.address_label')}</div>
              <p style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-18)', lineHeight: 1.7, color: 'var(--text-strong)', margin: 0 }}>
                {window.RH_STAMM.nameGrad}<br />{window.RH_STAMM.strasse}<br />{window.RH_STAMM.ort}
              </p>
              <a href={'mailto:' + window.RH_STAMM.email} style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-15)', color: 'var(--link)', display: 'inline-block', marginTop: 14, padding: '11px 0', margin: '3px 0 -11px' }}>{window.RH_STAMM.email}</a>
              <br />
              <a href={'tel:' + window.RH_STAMM.telefonHref} style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-15)', color: 'var(--link)', display: 'inline-block', padding: '11px 0', margin: '3px 0 -8px' }}>{window.RH_STAMM.telefon}</a>
              <br />
              <a href={window.RH_maps(window.RH_STAMM.mapsQuery)} target="_blank" rel="noopener"
                style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--text-muted)', display: 'inline-block', marginTop: 8, padding: '12px 0', margin: '-4px 0 -12px' }}>{t('detail.map')} ↗</a>
            </div>
            <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', padding: '24px 26px', background: 'var(--surface-card)' }}>
              <div className="rh-label" style={{ color: 'var(--messing-dunkel)', marginBottom: 14 }}>{t('contact.parish_label')}</div>
              <p style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-17)', lineHeight: 1.7, color: 'var(--text-body)', margin: 0 }}>
                {t.f({ de: 'Ev.-Luth. Kirchspiel', en: 'Ev. Lutheran Parish', cs: 'Ev.-luter. farnost' })}<br />Osterzgebirge - Region Altenberg
              </p>
            </div>
          </aside>
        </div>
      </section>
    </div>
  );
}

/* ---------------- Mediathek ---------------- */
function MediathekPage({ onNav, t }) {
  const tracks = window.RH_AUDIO || [];
  const links = window.RH_AUDIO_LINKS || {};
  // Videos entstehen nach dem Konzert → aus vergangenen Konzerten, sonst kommende.
  const past = (window.RH_DATA.past || []);
  const vids = (past.length ? past : (window.RH_DATA.concerts || [])).slice(0, 6);
  const [playing, setPlaying] = React.useState(null);
  const [prog, setProg] = React.useState(0);
  const engineRef = React.useRef(null);
  const rafRef = React.useRef(0);
  const featured = tracks[0];
  const rest = tracks.slice(1);
  // Real playback — mirrors the Home page: a genuine <audio> when a src exists,
  // otherwise the shared organ-pad placeholder, always with a moving progress bar.
  // A play button must never claim to play in silence.
  const parseLen = (s) => { const p = String(s || '0:0').split(':').map(Number); return (p[0] * 60) + (p[1] || 0); };
  const stopAll = React.useCallback(() => {
    if (engineRef.current) { try { engineRef.current.stop(); } catch (e) {} engineRef.current = null; }
    cancelAnimationFrame(rafRef.current);
  }, []);
  React.useEffect(() => () => stopAll(), [stopAll]);
  const play = (i) => {
    if (playing === i) { stopAll(); setPlaying(null); setProg(0); return; }
    stopAll();
    const tr = tracks[i]; const total = parseLen(tr && tr.len) || 1; const start = performance.now();
    setPlaying(i); setProg(0);
    if (tr && tr.src) {
      const a = new Audio(tr.src); a.play().catch(() => {}); engineRef.current = { stop() { try { a.pause(); } catch (e) {} } };
      const tick = () => { const d = a.duration || total; setProg(Math.min(1, (a.currentTime || 0) / d)); if (a.ended) { setPlaying(null); setProg(0); return; } rafRef.current = requestAnimationFrame(tick); };
      rafRef.current = requestAnimationFrame(tick);
    } else {
      engineRef.current = (window.rhMakePad ? window.rhMakePad((tr && tr.root) || 220) : { stop() {} });
      const tick = () => { const p = Math.min(1, ((performance.now() - start) / 1000) / total); setProg(p); if (p >= 1) { stopAll(); setPlaying(null); setProg(0); return; } rafRef.current = requestAnimationFrame(tick); };
      rafRef.current = requestAnimationFrame(tick);
    }
  };
  return (
    <div>
      <PageHeroDark t={t} eyebrow={t('nav.media')}
        title={t.f({ de: 'Mediathek', en: 'Media', cs: 'Mediatéka' })}
        lead={t.f({ de: 'Aufnahmen und Videos — nach Werk und Konzert geordnet.', en: 'Recordings and videos — ordered by work and concert.', cs: 'Nahrávky a videa — řazené podle skladby a koncertu.' })}
        image={window.RH_IMG.gloria} caption={t.f({ de: 'Jessica Zappe · „Gloria in excelsis Deo“', en: 'Jessica Zappe · “Gloria in excelsis Deo”', cs: 'Jessica Zappe · „Gloria in excelsis Deo“' })} />

      {/* Aufnahmen — ein hervorgehobenes Stück + die Liste */}
      <section style={{ background: 'var(--pergament)' }}>
        <div style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: '80px var(--gutter) 32px' }}>
          <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 24, flexWrap: 'wrap', marginBottom: 28 }}>
            <div>
              <div className="rh-eyebrow" style={{ marginBottom: 16 }}>{t('audio.eyebrow')}</div>
              <h2 style={{ fontSize: 'clamp(1.9rem, 1.4rem + 1.4vw, 2.6rem)', margin: 0 }}>{t('audio.title')}.</h2>
            </div>
            <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
              <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)' }}>{tracks.length} {t.f({ de: 'Aufnahmen', en: 'recordings', cs: 'nahrávek' })}</span>
              {links.spotify && (
                <a href={links.spotify} target="_blank" rel="noopener" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--messing-dunkel)', textDecoration: 'none', border: '1px solid var(--messing)', borderRadius: 999, padding: '8px 16px' }}>
                  <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm4.6 14.4a.6.6 0 0 1-.9.2c-2.3-1.4-5.2-1.7-8.6-.9a.6.6 0 1 1-.3-1.2c3.7-.8 7-.5 9.6 1.1a.6.6 0 0 1 .2.8zm1.2-2.7a.75.75 0 0 1-1 .3c-2.6-1.6-6.6-2.1-9.7-1.1a.75.75 0 1 1-.4-1.4c3.5-1.1 7.9-.6 10.9 1.2a.75.75 0 0 1 .2 1zm.1-2.8C14.8 9 8.9 8.8 5.6 9.8a.9.9 0 1 1-.5-1.7C8.9 7 15.4 7.2 18.9 9.3a.9.9 0 1 1-.9 1.6z"/></svg>
                  {t('audio.spotify')}
                </a>
              )}
            </div>
          </div>

          {featured && (
            <div style={{ display: 'grid', gridTemplateColumns: '0.9fr 1.1fr', gap: 40, alignItems: 'start', marginBottom: 40, background: 'var(--surface-card)', border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', overflow: 'hidden' }} className="rh-media-featured">
              <div style={{ position: 'relative', aspectRatio: '4 / 3', overflow: 'hidden' }}>
                <img loading="lazy" decoding="async" src={window.RH_IMG.organStops} alt="" aria-hidden="true" style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'saturate(0.96)' }} />
                <button onClick={() => play(0)} aria-label="Play"
                  style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', width: 72, height: 72, borderRadius: '50%', border: '2px solid var(--pergament-text)', color: 'var(--pergament-text)', background: 'rgba(26,24,19,0.42)', cursor: 'pointer', fontSize: 'var(--fs-26)', display: 'grid', placeItems: 'center', paddingLeft: playing === 0 ? 0 : 4 }}>{playing === 0 ? '❚❚' : '▶'}</button>
                {playing === 0 && (
                  <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: 4, background: 'rgba(255,255,255,0.28)' }}>
                    <div style={{ height: '100%', width: (prog * 100) + '%', background: 'var(--messing)', transition: 'width 120ms linear' }} />
                  </div>
                )}
              </div>
              <div style={{ padding: '28px 34px 28px 0' }}>
                <div className="rh-label" style={{ color: 'var(--messing-dunkel)', marginBottom: 12 }}>{t.f({ de: 'Empfohlene Aufnahme', en: 'Featured recording', cs: 'Doporučená nahrávka' })}</div>
                <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'var(--fs-28)', color: 'var(--text-strong)', lineHeight: 1.15 }}>{t.f(featured.title)}</div>
                <div className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'var(--text-muted)', marginTop: 8 }}>{featured.composer} · {t.f(featured.meta)} · {featured.len}</div>
              </div>
            </div>
          )}

          <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', overflow: 'hidden', background: 'var(--surface-card)' }}>
            {rest.map((tr, i) => {
              const idx = i + 1;
              const on = playing === idx;
              return (
                <div key={idx} style={{ position: 'relative', display: 'grid', gridTemplateColumns: '48px 1fr auto', gap: 18, alignItems: 'center', padding: '16px 20px', borderBottom: i < rest.length - 1 ? '1px solid var(--border-hairline)' : 'none', background: on ? 'var(--pergament)' : 'transparent' }}>
                  <button onClick={() => play(idx)} aria-label="Play" style={{ width: 44, height: 44, borderRadius: '50%', border: '1.5px solid var(--messing)', color: 'var(--messing-dunkel)', background: 'none', cursor: 'pointer', display: 'grid', placeItems: 'center', fontSize: 'var(--fs-15)', paddingLeft: on ? 0 : 2 }}>{on ? '❚❚' : '▶'}</button>
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'var(--fs-20)', color: 'var(--text-strong)' }}>{t.f(tr.title)}</div>
                    <div className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)', marginTop: 2 }}>{tr.composer} · {t.f(tr.meta)}</div>
                  </div>
                  <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)' }}>{tr.len}</span>
                  {on && <div style={{ position: 'absolute', left: 0, bottom: 0, height: 3, width: (prog * 100) + '%', background: 'var(--messing)', transition: 'width 120ms linear' }} />}
                </div>
              );
            })}
          </div>
        </div>
      </section>

      <section style={{ background: 'var(--pergament)' }}>
        <div style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: '16px var(--gutter) 110px' }}>
          <div className="rh-eyebrow" style={{ marginBottom: 16 }}>{t.f({ de: 'Video', en: 'Video', cs: 'Video' })}</div>
          <h2 style={{ fontSize: 'clamp(1.9rem, 1.4rem + 1.4vw, 2.6rem)', margin: '0 0 26px' }}>{t.f({ de: 'Aufzeichnungen.', en: 'Recordings.', cs: 'Záznamy.' })}</h2>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 24 }} className="rh-media-grid">
            {vids.map((c) => (
              <figure key={c.id} style={{ margin: 0, cursor: 'pointer' }} onClick={() => onNav('detail', c.id)}>
                <div style={{ position: 'relative', aspectRatio: '16 / 9', borderRadius: 'var(--radius-md)', overflow: 'hidden', border: '1px solid var(--border-hairline)' }}>
                  <img loading="lazy" decoding="async" src={c.image} alt="" aria-hidden="true" style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'saturate(0.96)' }} />
                  <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: 54, height: 54, borderRadius: '50%', border: '2px solid var(--pergament-text)', color: 'var(--pergament-text)', display: 'grid', placeItems: 'center', background: 'rgba(26,24,19,0.35)' }}>▶</span>
                </div>
                <figcaption style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-caption)', color: 'var(--text-muted)', marginTop: 8 }}>— {t.f(c.title)}</figcaption>
              </figure>
            ))}
          </div>
          <p style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)', marginTop: 18 }}>{t.f({ de: 'Videos werden im erweiterten Datenschutzmodus geladen (erst auf Klick).', en: 'Videos load in enhanced privacy mode (only on click).', cs: 'Videa se načítají v rozšířeném režimu ochrany soukromí (až po kliknutí).' })}</p>
        </div>
      </section>
    </div>
  );
}

function ImageCreditsPage({ onNav, t }) {
  const { Ornament, Button } = cpNS;
  const groups = window.RH_KY || [];
  const LIC = window.RH_LIC || {};
  const L = (t && t.lang) || 'de';
  const licTxt = (k) => { const M = LIC[k || 'pd']; return M ? (M[L] || M.de) : ''; };
  const src = window.RH_ART_SOURCE || 'Wikimedia Commons';
  const eyebrow = { de: 'Rechtliches', en: 'Legal', cs: 'Právní informace' }[L];
  const title = { de: 'Bildnachweise', en: 'Image credits', cs: 'Obrazové zdroje' }[L];
  const stand = { de: 'Stand: Juli 2026', en: 'As of July 2026', cs: 'Stav: červenec 2026' }[L];
  return (
    <div>
      <section style={{ position: 'relative', overflow: 'hidden', color: 'var(--pergament-text)', background: 'var(--scrim-1)' }}>
        <div style={{ position: 'relative', maxWidth: 'var(--container)', margin: '0 auto', padding: '120px var(--gutter) 56px' }}>
          <div className="rh-eyebrow rh-eyebrow--dark" style={{ marginBottom: 16 }}>{eyebrow}</div>
          <h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(2.4rem, 1.8rem + 2.4vw, 3.8rem)', color: 'var(--pergament-text)', margin: 0 }}>{title}.</h1>
        </div>
      </section>
      <section className="rh-flat" style={{ background: 'var(--pergament)' }}>
        <div style={{ maxWidth: '64ch', margin: '0 auto', padding: '64px var(--gutter) 40px' }}>
          <p className="rh-dropcap" style={{ fontSize: 'var(--fs-19)', lineHeight: 1.75, color: 'var(--text-body)', marginTop: 0, marginBottom: 8 }}>
            {t.f({
              de: 'Die auf dieser Seite verwendeten Gemälde dienen als künstlerischer Rahmen für die musikalische Arbeit. Es handelt sich um Werke der bildenden Kunst, deren Urheber seit mehr als 70 Jahren verstorben sind — sie sind gemeinfrei. Reproduktionen stammen aus frei zugänglichen Quellen.',
              en: 'The paintings used on this site frame the musical work. They are works of art whose creators died more than 70 years ago — they are in the public domain. Reproductions are from freely accessible sources.',
              cs: 'Obrazy použité na těchto stránkách tvoří umělecký rámec hudební práce. Jde o výtvarná díla, jejichž autoři zemřeli před více než 70 lety — jsou volným dílem. Reprodukce pocházejí z volně dostupných zdrojů.',
            })}
          </p>
          <div style={{ marginTop: 44, display: 'grid', gap: 34 }}>
            {groups.map((grp) => (
              <div key={grp.id}>
                <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'clamp(1.4rem, 1.1rem + 0.8vw, 1.8rem)', margin: '0 0 14px', color: 'var(--text-strong)' }}>{t.f(grp.label)}.</h2>
                <div>
                  {grp.images.map((im, k) => (
                    <div key={k} style={{ display: 'flex', gap: 12, alignItems: 'baseline', padding: '13px 0', borderBottom: '1px solid var(--border-hairline)' }}>
                      <span aria-hidden="true" style={{ color: 'var(--messing)', flex: 'none' }}>—</span>
                      <span style={{ flex: 1, minWidth: 0 }}>
                        <span style={{ display: 'block', fontFamily: 'var(--font-body)', fontSize: 'var(--fs-16)', color: 'var(--text-strong)' }}>{im.artist}</span>
                        <span style={{ display: 'block', fontFamily: 'var(--font-display)', fontStyle: 'italic', fontSize: 'var(--fs-16-5)', color: 'var(--text-body)', lineHeight: 1.3, marginTop: 1 }}>{t.f(im.title)}</span>
                        {/* Jahr und Museum, wo bekannt — fachlich der eigentliche Nachweis. */}
                        {(im.jahr || im.museum) && (
                          <span style={{ display: 'block', fontFamily: 'var(--font-body)', fontSize: 'var(--fs-14-5)', color: 'var(--text-muted)', marginTop: 3 }}>{[im.jahr, im.museum].filter(Boolean).join(' · ')}</span>
                        )}
                        <span style={{ display: 'block', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12)', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--text-muted)', marginTop: 5 }}>
                          {licTxt(im.lic) + ' · '}
                          {im.src && /^https?:/.test(im.src)
                            ? <a href={im.src} target="_blank" rel="noopener" style={{ color: 'var(--messing-dunkel)', textDecoration: 'none', borderBottom: '1px solid var(--messing)' }}>{src}</a>
                            : src}
                        </span>
                      </span>
                    </div>
                  ))}
                </div>
              </div>
            ))}
          </div>
          <p style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-17-5)', lineHeight: 1.7, color: 'var(--text-body)', maxWidth: '60ch', marginTop: 40 }}>
            {t.f({
              de: 'Sollte trotz sorgfältiger Prüfung ein Werk nicht korrekt zugeordnet sein, genügt ein Hinweis — wir korrigieren oder entfernen es umgehend.',
              en: 'Should a work be attributed incorrectly despite careful checking, a short note is enough — we will correct or remove it promptly.',
              cs: 'Pokud by přes pečlivou kontrolu bylo některé dílo uvedeno nesprávně, stačí upozornění — neprodleně je opravíme nebo odstraníme.',
            })}
          </p>
          <Ornament mark="leaf" style={{ margin: '56px auto 40px' }} />
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 16 }}>
            <span style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'var(--text-muted)' }}>{stand}</span>
            <Button variant="outline" onClick={() => onNav('impressum')}>{{ de: 'Zum Impressum', en: 'To the legal notice', cs: 'K impresu' }[L]} →</Button>
          </div>
        </div>
      </section>
    </div>
  );
}

Object.assign(window, { VitaPage, EditionPage, InstrumentsPage, SoonPage, LegalPage, KontaktPage, MediathekPage, ImageCreditsPage, InstrumentFigure });
