// Home — immersive, Baroque-led homepage. Real imagery; all copy via `t`.
// Kantor leads; an authentic portrait+quote replaces the old floating text;
// the Orgelsommer recurs; newsletter + social invite people in; a scripture
// verse threads through (Roy is a cantor).
const homeNS = window.RoyHeyneDesignSystem_df1161;

function Hero({ onNav, t }) {
  const { Button } = homeNS;
  const I = window.RH_IMG;
  const roles = t('hero.roles'); // [Kantor, Organist, Cembalist]
  return (
    <section style={{ position: 'relative', minHeight: '560px', display: 'flex', alignItems: 'flex-start',
      overflow: 'hidden', background:
        'radial-gradient(115% 95% at 76% 28%, rgba(124,99,52,0.42), transparent 56%), radial-gradient(90% 90% at 8% 82%, rgba(107,42,50,0.20), transparent 60%), linear-gradient(158deg, var(--scrim-warm-top) 0%, var(--scrim-warm-bot) 64%)' }}>
      {(() => {
        const s = window.RH_slot ? window.RH_slot('home-hero', { src: I.shepherds, imgPosition: 'center 38%' }) : null;
        const src = s ? s.src : I.shepherds;
        const op = s && s.imgPosition ? s.imgPosition : 'center 38%';
        const zoom = s && s.zoom ? s.zoom : 1;
        return (
          // Das Kopfbild ist das erste, was ein Besucher sieht — hier wäre
          // `lazy` ein Fehler: der Browser würde mit dem Laden warten, bis er
          // weiss, dass es sichtbar ist. Vorrang statt Aufschub.
          <img loading="eager" decoding="async" fetchpriority="high" src={src} alt="" aria-hidden="true"
            style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover',
              objectPosition: op, transform: zoom !== 1 ? 'scale(' + zoom + ')' : undefined, transformOrigin: op, filter: 'saturate(1.04) contrast(1.02)' }} />
        );
      })()}
      <div style={{ position: 'absolute', inset: 0, background:
        'linear-gradient(96deg, rgba(18,16,12,0.94) 0%, rgba(18,16,12,0.82) 34%, rgba(18,16,12,0.5) 60%, rgba(18,16,12,0.12) 86%)' }} />
      <div style={{ position: 'absolute', inset: 0, background:
        'linear-gradient(0deg, rgba(18,16,12,0.62), rgba(18,16,12,0.16) 40%, transparent 58%)' }} />
      {/* Mobil: Text spannt über die volle Breite in die helle Bildzone — der 96°-Scrim
          verdunkelt nur links. Dieser Layer greift NUR ≤768px und legt die Textzone
          gleichmäßig dunkel, ohne das Gemälde am Desktop anzutasten (AA bei 390px). */}
      <div aria-hidden="true" className="rh-hero-scrim-m" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }} />
      <div aria-hidden="true" style={{ position: 'absolute', inset: 18, border: '1px solid rgba(156,124,67,0.34)', borderRadius: 'var(--radius-md)', pointerEvents: 'none' }} />

      <div style={{ position: 'relative', maxWidth: 'var(--container-wide)', width: '100%', margin: '0 auto', padding: 'clamp(120px, 15vh, 180px) calc(var(--gutter) + 20px) clamp(80px, 12vh, 150px)' }}>
        <div style={{ maxWidth: 'min(640px, 100%)' }}>
          <div className="rh-eyebrow rh-eyebrow--dark" style={{ marginBottom: 24 }}>{t('hero.eyebrow')}</div>
          {/* Name dominates. Padding-bottom contains the descender of "y" so it can
              never touch the role line, whatever the wrap; margin sets the calm gap. */}
          <h1 style={{ fontFamily: 'var(--font-display)', fontWeight: 600,
            fontSize: 'clamp(3.4rem, 1.8rem + 5vw, 6rem)', lineHeight: 1.05, letterSpacing: '-0.022em',
            color: 'var(--pergament-text)', margin: '0 0 clamp(28px, 3vw, 42px)', paddingBottom: '0.14em',
            textWrap: 'balance', fontFeatureSettings: 'var(--feat-display)' }}>
            Roy Heyne
          </h1>
          {/* Roles — (b) Kantorat quietly first. ONE mean only: warm brass on "Kantor".
              Uniform separators: identical mid-dots and identical gap around every item, so
              the zeile reads as composed, not as two mixed systems. Rank is carried solely
              by brass on the Kantorat; "Organist · Cembalist" stay equal activities. */}
          <div style={{ display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: '0 12px',
            marginBottom: 'clamp(22px, 2.6vw, 30px)',
            fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500,
            fontSize: 'clamp(1.15rem, 0.95rem + 0.7vw, 1.5rem)' }}>
            <span style={{ color: 'var(--messing)' }}>{roles[0]}</span>
            <span aria-hidden="true" style={{ color: 'var(--messing)', opacity: 0.55 }}>·</span>
            <span style={{ color: 'rgba(236,230,216,0.74)' }}>{roles[1]}</span>
            <span aria-hidden="true" style={{ color: 'var(--messing)', opacity: 0.55 }}>·</span>
            <span style={{ color: 'rgba(236,230,216,0.74)' }}>{roles[2]}</span>
          </div>
          <p style={{ fontFamily: 'var(--font-body)', fontSize: 'clamp(1.05rem, 1rem + 0.4vw, 1.3rem)', lineHeight: 1.75,
            color: 'rgba(236,230,216,0.9)', maxWidth: '40ch', margin: 0, minHeight: 'calc(1.75em * 5)' }}>
            {/* hero.lead ist eine LISTE von Zeilen — jede Auskunft eine Zeile.
                Der Umbruch steht damit im Text und nicht in der Komponente; jede
                Sprache bricht an ihrer eigenen richtigen Stelle, und alle drei
                haben gleich viele Knoten (Sprach-Parität). */}
            {(Array.isArray(t('hero.lead')) ? t('hero.lead') : [t('hero.lead')]).map((zeile, i) => (
              <React.Fragment key={i}>{i > 0 && <br />}{zeile}</React.Fragment>
            ))}
          </p>
          <div style={{ display: 'flex', gap: 16, marginTop: 38, alignItems: 'center', flexWrap: 'wrap' }}>
            <Button variant="inverse" size="lg" onClick={() => onNav('konzerte')}>{t('hero.cta1')}</Button>
            <Button variant="ghost" size="lg" onClick={() => { const el = document.getElementById('rh-klang'); if (el) { const top = el.getBoundingClientRect().top + window.pageYOffset; try { window.scrollTo({ top, behavior: 'smooth' }); } catch (e) {} setTimeout(() => { if (Math.abs(window.pageYOffset - top) > 4) window.scrollTo(0, top); }, 420); } }}
              style={{ color: 'var(--pergament-text)' }} iconAfter={<span>→</span>}>{t('hero.cta2')}</Button>
          </div>
        </div>
      </div>

      <div style={{ position: 'absolute', right: 30, bottom: 22, textAlign: 'right', maxWidth: '40ch',
        fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-caption)', color: 'rgba(236,230,216,0.62)', letterSpacing: '0.02em' }}>
        {t('hero.caption')}
      </div>
    </section>
  );
}

// An authentic portrait + first-person quote, anchored by a scripture verse.
function Portrait({ t }) {
  const { ImagePlate } = homeNS;
  return (
    <section style={{ background: 'var(--pergament)', borderBottom: '1px solid var(--border-hairline)' }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: 'clamp(60px, 8vw, 120px) var(--gutter)',
        display: 'grid', gridTemplateColumns: '0.78fr 1.22fr', gap: 'clamp(32px, 5vw, 64px)', alignItems: 'center' }}>
        {/* alignItems:center ist hier paritaetssicher: Zitat (leere Reserve) und
           Bild (festes Ratio) sind in DE/EN/CZ exakt gleich hoch — kein Springen. */}
        <ImagePlate src={window.RH_IMG.portrait} slot="home-portrait" ratio="4 / 5" arch
          caption={t.f({ de: 'Roy Heyne an der Ranft-Orgel · Geising · Foto: Daniel Förster', en: 'Roy Heyne at the Ranft organ · Geising · Photo: Daniel Förster', cs: 'Roy Heyne u Ranftových varhan · Geising · Foto: Daniel Förster' })} />
        <div>
          {window.RH_ParityText({ as: 'p', lang: t.lang, all: window.RH_ALL('manifest.quote'), style: { marginBottom: 28 }, textStyle: { fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.8rem, 1.2rem + 2.2vw, 2.9rem)', lineHeight: 1.26, color: 'var(--text-strong)', textWrap: 'balance' } })}
          <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
            <span aria-hidden="true" style={{ width: 34, height: 1, background: 'var(--messing)' }} />
            <span>
              <span style={{ display: 'block', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-15)', fontWeight: 600, color: 'var(--text-strong)' }}>{t('manifest.author')}</span>
              <span style={{ display: 'block', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13-5)', color: 'var(--text-muted)' }}>{t('manifest.role')}</span>
            </span>
          </div>
        </div>
      </div>
    </section>
  );
}

function UpcomingConcerts({ onNav, t }) {
  const { ConcertCard, SectionHeading, Button, Badge } = homeNS;
  const osAll = window.RH_DATA.all.filter(window.RH_istOeffentlich).find((x) => x.id === 'geising-0603');
  // Flaggschiff nur zeigen, solange die Saison nicht vorbei ist (sonst rutscht es
  // automatisch ins Archiv — wie jedes andere Konzert auch).
  const os = (osAll && window.RH_DATA.concerts.filter(window.RH_istOeffentlich).some((c) => c.id === 'geising-0603')) ? osAll : null;
  const concerts = window.RH_DATA.concerts.filter(window.RH_istOeffentlich).filter((c) => c.id !== 'geising-0603').slice(0, 3);
  const nextYear = new Date().getFullYear() + 1;
  // Eine laufende Saison soll auch als laufend lesen — Zustand aus RH_orgelsommerState().
  const osState = window.RH_orgelsommerState ? window.RH_orgelsommerState() : null;
  const osRunning = !!(osState && osState.phase === 'laeuft');
  const osCur = window.RH_ORGELSOMMER_CURRENT;
  const osWeeks = (window.RH_ORGELSOMMER || []).length;
  const osLive = osRunning && osCur && osWeeks
    ? t.f({ de: 'Läuft jetzt · Woche ' + osCur.week + ' von ' + osWeeks,
            en: 'Running now · week ' + osCur.week + ' of ' + osWeeks,
            cs: 'Právě probíhá · ' + osCur.week + '. z ' + osWeeks + ' týdnů' })
    : '';
  const isEmpty = !os && concerts.length === 0;
  const tick = (pos) => (
    <span aria-hidden="true" style={{ position: 'absolute', width: 13, height: 13,
      ...(pos.t != null ? { top: 9 } : { bottom: 9 }), ...(pos.l != null ? { left: 9 } : { right: 9 }),
      [pos.t != null ? 'borderTop' : 'borderBottom']: '1px solid var(--messing)',
      [pos.l != null ? 'borderLeft' : 'borderRight']: '1px solid var(--messing)', opacity: 0.85 }} />
  );
  return (
    <section style={{ background: 'var(--pergament)' }}>
      <div style={{ maxWidth: 'var(--container-wide)', margin: '0 auto', padding: 'clamp(60px, 8vw, 120px) var(--gutter)' }}>
        <SectionHeading label={t('concerts.label')} title={t('concerts.title')} lead={t('concerts.lead')} leadAll={window.RH_ALL('concerts.lead')} lang={t.lang} style={{ marginBottom: 40 }} />

        {/* Flaggschiff — der Geisinger Orgelsommer, aus der eigenen Vollsektion in die
            Konzerte gefaltet, aber sichtbar erhöht über den Einzelterminen: der echte
            Stich, das Pressezitat und die Fakten-Chips bleiben. */}
        {os && (
          <div style={{ display: 'grid', gridTemplateColumns: 'clamp(190px, 24vw, 260px) 1fr', gap: 'clamp(28px, 4vw, 56px)',
            alignItems: 'start', marginBottom: 30, position: 'relative', padding: 'clamp(24px, 3vw, 42px)',
            background: 'radial-gradient(150% 130% at 10% 8%, rgba(156,124,67,0.12), transparent 62%), var(--pergament-hell)',
            border: '1px solid var(--messing)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--elev-2)' }}>
            {/* Das Plakat füllt seinen Rahmen randlos: kein Passepartout, keine
                Luft ringsum — der Stich reicht bis an die Messing-Kontur. Das
                Plakat ist fast quadratisch, die Textspalte daneben höher; der
                Rest der Tafel trägt darum EXAKT das Plakatgelb (#FFF2CD, am
                Bildrand gemessen). So ist die Tafel gefüllt, ohne den Schriftzug
                „Orgelsommer“ zu beschneiden — ein Plakat ohne Titel ist keins. */}
            <figure style={{ margin: 0, background: '#FFF2CD', border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-sm)', padding: 0, position: 'relative', alignSelf: 'stretch', overflow: 'hidden', lineHeight: 0 }}>
              <img loading="lazy" decoding="async" src={window.RH_IMG.orgelsommer} alt="Geisinger Orgelsommer — Stich der Ranft-Orgel von 1757"
                style={{ width: '100%', height: 'auto', display: 'block' }} />
              {tick({ t: 1, l: 1 })}{tick({ t: 1, r: 1 })}{tick({ b: 1, l: 1 })}{tick({ b: 1, r: 1 })}
            </figure>
            <div style={{ minWidth: 0 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 14 }}>
                <span className="rh-eyebrow">{t('feature.eyebrow')}</span>
                {osLive && (
                  <Badge tone="accent" style={{ gap: 6 }}>
                    <span aria-hidden="true" className="rh-live-dot" />{osLive}
                  </Badge>
                )}
              </div>
              <h3 style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'clamp(1.7rem, 1.3rem + 1.4vw, 2.5rem)', margin: '0 0 8px', color: 'var(--text-strong)', lineHeight: 1.1 }}>{t.f(os.series)}.</h3>
              {/* Blocksatz in der rechten Spalte: Meta-Zeile und Pressezitat laufen
                  über zwei bis drei Zeilen und franzten rechts stark aus. Mit
                  Silbentrennung (⁠hyphens⁠) statt ohne — Blocksatz ohne Trennung
                  reißt Flüsse in den Satz. Kurze Einzeiler bleiben linksbündig. */}
              <p className="rh-num" lang={t.lang} style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14-5)', color: 'var(--text-muted)', margin: '0 0 18px', lineHeight: 1.55, textAlign: 'justify', hyphens: 'auto', WebkitHyphens: 'auto' }}>{t.f(os.recurring)}</p>
              {os.press && (
                <blockquote style={{ margin: '0 0 20px', borderLeft: '2px solid var(--messing)', paddingLeft: 16 }}>
                  <p lang={t.lang} style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.1rem, 0.95rem + 0.6vw, 1.4rem)', lineHeight: 1.35, color: 'var(--text-strong)', margin: 0, textAlign: 'justify', hyphens: 'auto', WebkitHyphens: 'auto' }}>„{t.f(os.press)}“</p>
                  <footer style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12-5)', color: 'var(--text-muted)', marginTop: 8 }}>Roy Heyne · {t.f(os.pressSource)}</footer>
                </blockquote>
              )}
              <p className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13-5)', color: 'var(--text-muted)', margin: '0 0 22px', lineHeight: 1.5 }}>{t.f({ de: 'Eintritt frei · Spenden für den Orgelerhalt', en: 'Free admission · donations for the organ’s upkeep', cs: 'Vstup volný · dary na údržbu varhan' })}</p>
              <Button variant="primary" onClick={() => onNav('detail', os.id)}>{t('feature.cta')}</Button>
            </div>
          </div>
        )}

        {/* Weitere Einzeltermine — nur wenn welche da sind */}
        {concerts.length > 0 && (
        <React.Fragment>
        {/* Kein vages „Weitere Termine": es sind immer genau die nächsten n —
            also sagt die Zeile das auch, und zwar zählend aus den echten Daten. */}
        <div className="rh-label" style={{ display: 'flex', alignItems: 'center', gap: 16, color: 'var(--text-muted)', margin: os ? '46px 0 26px' : '0 0 26px' }}>
          <span>{(() => {
            const n = concerts.length;
            const w = { de: ['', '', 'zwei', 'drei', 'vier', 'fünf'], en: ['', '', 'two', 'three', 'four', 'five'], cs: ['', '', 'dva', 'tři', 'čtyři', 'pět'] };
            if (n === 1) return t.f({ de: 'Der nächste Termin', en: 'The next date', cs: 'Nejbližší termín' });
            const z = (l) => w[l][n] || String(n);
            return t.f({ de: 'Die nächsten ' + z('de') + ' Termine', en: 'The next ' + z('en') + ' dates', cs: 'Nejbližší ' + z('cs') + ' termíny' });
          })()}</span>
          <span aria-hidden="true" style={{ flex: 1, height: 1, background: 'var(--border-hairline)' }} />
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 'var(--space-6)', alignItems: 'stretch' }}>
          {concerts.map((c) => (
            <div key={c.id} style={{ cursor: 'pointer', height: '100%', minWidth: 0 }}
              onClickCapture={(e) => { e.preventDefault(); onNav('detail', c.id); }}>
              <ConcertCard
                iso={c.iso} lang={t.lang} conf={window.RH_confOf ? window.RH_confOf(c) : 'ev'} anlass={c.anlass ? t.f(c.anlass) : undefined} anlassFarbe={c.anlassFarbe}
                day={c.day} mon={t.f(c.mon)} weekday={t.f(c.weekday)} time={c.time}
                title={t.f(c.title)} place={t.f(c.place)} city={t.f(c.city)}
                programme={(c.works && c.works.length) ? c.works.slice(0, 3).map((w) => String(w[0]).split(' ').pop()).join(' · ') : undefined}
                instrument={t.f(c.instrument)} status={c.status} statusLabel={t('status.' + c.status)}
                image={c.image} porthole={c.porthole} portholeZoom={c.portholeZoom} portholePos={c.portholePos} imgPosition={c.imgPosition} zoom={c.zoom} lit={(window.RH_LITURGY && (window.RH_LITURGY.colorForConcert(c) || {}).hex) || undefined} href="#" />
            </div>
          ))}
        </div>
        </React.Fragment>
        )}

        {/* Leerzustand: keine kommenden Termine — nie eine leere Sektion, sondern
            ein ruhiger Verweis ins Archiv und auf die kommende Saison. */}
        {isEmpty && (
          <div style={{ textAlign: 'center', padding: 'clamp(40px, 6vw, 80px) 0', border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', background: 'var(--pergament-hell)' }}>
            <div aria-hidden="true" style={{ fontFamily: 'var(--font-display)', fontSize: 'var(--fs-30)', color: 'var(--messing)', marginBottom: 16 }}>❧</div>
            <p style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.3rem, 1rem + 1vw, 1.8rem)', color: 'var(--text-strong)', margin: '0 0 10px', textWrap: 'balance' }}>{t.f({ de: 'Die Konzertsaison ' + nextYear + ' wird vorbereitet.', en: 'The ' + nextYear + ' concert season is being prepared.', cs: 'Koncertní sezóna ' + nextYear + ' se připravuje.' })}</p>
            <p style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-16)', color: 'var(--text-muted)', margin: '0 auto 24px', maxWidth: '46ch', lineHeight: 1.6 }}>{t.f({ de: 'Neue Termine erscheinen hier, sobald sie feststehen. Bis dahin lohnt ein Blick ins Konzertarchiv — mit Aufzeichnungen vergangener Konzerte.', en: 'New dates will appear here as soon as they are confirmed. In the meantime, explore the concert archive — with recordings of past concerts.', cs: 'Nové termíny se zde objeví, jakmile budou potvrzeny. Do té doby nahlédněte do archivu koncertů — se záznamy minulých koncertů.' })}</p>
            <Button variant="primary" onClick={() => onNav('konzerte')}>{t.f({ de: 'Zum Konzertarchiv', en: 'To the concert archive', cs: 'Do archivu koncertů' })}</Button>
          </div>
        )}

        {!isEmpty && (
        <div style={{ display: 'flex', justifyContent: 'center', marginTop: 'clamp(40px, 5vw, 64px)' }}>
          <Button variant="outline" onClick={() => onNav('konzerte')} iconAfter={<span>→</span>}>{t('common.all_concerts')}</Button>
        </div>
        )}
      </div>
    </section>
  );
}

function Teasers({ onNav, t }) {
  const { Ornament, ImagePlate, Button, SectionHeading } = homeNS;
  const I = window.RH_IMG;
  // Triptychon (bewusste, freigegebene Änderung an B1): drei gleichwertige
  // Bogen-Tafeln — umgesetzt INNERHALB des Marken-Bildtreatments: ImagePlate mit
  // Passepartout, barockem Rundbogen (arch), Tafel-Nummer und Bildunterschrift;
  // Button-Komponente als CTA, barocker Schlusspunkt. Randlos = Apple-Ruhe + Luft.
  const ways = [
    { key: 'vita', no: 'I', label: t('teaser.vita_label'), title: t('teaser.vita_title'),
      text: t('teaser.vita_text'), textKey: 'teaser.vita_text', cta: t('teaser.vita_cta'), img: I.kings, caption: t.f({ de: 'Jessica Zappe · Die Heiligen Drei Könige', en: 'Jessica Zappe · The Three Kings', cs: 'Jessica Zappe · Tři králové' }) },
    { key: 'instrumente', no: 'II', label: t('teaser.instr_label'), title: t('teaser.instr_title'),
      text: t('teaser.instr_text'), textKey: 'teaser.instr_text', cta: t('teaser.instr_cta'), img: I.cembaloFull, caption: t.f({ de: 'Cembalo nach Christian Vater, 1738', en: 'Harpsichord after Christian Vater, 1738', cs: 'Cembalo podle Christiana Vatera, 1738' }) },
    { key: 'edition', no: 'III', label: t('interlude.eyebrow'), title: t('interlude.title'),
      text: t('interlude.text'), textKey: 'interlude.text', cta: t('interlude.cta'), img: I.editionCover, caption: t.f({ de: 'Böhmische Pastorellen · Kamprad, 2023', en: 'Bohemian pastorellas · Kamprad, 2023', cs: 'České pastorely · Kamprad, 2023' }) },
  ];
  return (
    <section style={{ background: 'var(--pergament)' }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: 'clamp(60px, 8vw, 120px) var(--gutter)' }}>
        <SectionHeading label={t('teaser.sec_label')} title={t('teaser.sec_title')} lead={t('teaser.sec_lead')} leadAll={window.RH_ALL('teaser.sec_lead')} lang={t.lang} style={{ marginBottom: 40 }} />
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 'clamp(32px, 4vw, 64px)', alignItems: 'stretch' }}>
          {ways.map((w) => (
            <div key={w.key} style={{ display: 'flex', flexDirection: 'column', minWidth: 0 }}>
              <ImagePlate src={w.img} ratio="4 / 5" arch />
              {/* Caption website-seitig gerendert (laedt live, unabh. vom ds-Bundle):
                 feste 2-Zeilen-Reserve → alle drei Eyebrows exakt auf einer Hoehe,
                 zugleich sprachparitaetisch (DE/EN/CZ gleich hoch). */}
              <div style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-caption)', color: 'var(--text-muted)',
                marginTop: '0.7rem', lineHeight: 1.5, display: 'flex', gap: '0.5rem', alignItems: 'baseline',
                minHeight: 'calc(var(--fs-caption) * 1.5 * 2)' }}>
                <span aria-hidden="true" style={{ color: 'var(--messing)' }}>—</span>
                <span>{w.caption}</span>
              </div>
              <div className="rh-eyebrow" style={{ margin: '16px 0 10px' }}>
                <span style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', textTransform: 'none', letterSpacing: 0, fontSize: 'var(--fs-17)', marginRight: 2 }}>{w.no}.</span>
                {w.label}
              </div>
              <h3 style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'clamp(1.5rem, 1.2rem + 0.9vw, 1.95rem)', lineHeight: 1.14, color: 'var(--text-strong)', margin: '0 0 12px',
                // Zwei Zeilen reserviert, auch wenn der Titel eine braucht.
                // Sonst beginnt der Fließtext bei » Roy Heyne « höher als bei den
                // zweizeiligen Nachbarn, und die Karten stehen versetzt.
                // In em, damit die Reserve dem clamp() folgt — dasselbe Mittel,
                // das RH_ParityText für die Sprachparität benutzt.
                minHeight: 'calc(1.14em * 2)', display: 'flex', alignItems: 'start' }}>{w.title}.</h3>
              {window.RH_ParityText({ as: 'p', lang: t.lang, all: window.RH_ALL(w.textKey), style: { flex: 1, marginBottom: 24 }, textStyle: { fontSize: 'var(--fs-17)', lineHeight: 1.72, color: 'var(--text-body)' } })}
              <div>
                <Button variant="outline" onClick={() => onNav(w.key)}>{w.cta}</Button>
              </div>
            </div>
          ))}
        </div>
        <Ornament mark="leaf" style={{ margin: 'clamp(56px, 7vw, 96px) auto 0' }} />
      </div>
    </section>
  );
}

function Newsletter({ t, onNav }) {
  // Schlanke Anmelde-Zeile im Fußbereich — die frühere Vollsektion mit Bildplatte
  // wurde reduziert, damit die geistliche Coda (VerseBreak) den Bogen schließt.
  // Einzeilige Einladung, Double-Opt-in bleibt erhalten.
  const [email, setEmail] = React.useState('');
  const [consent, setConsent] = React.useState(false);
  const [sent, setSent] = React.useState(false);
  const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.trim());
  return (
    <section style={{ background: 'var(--pergament-hell)', borderTop: '1px solid var(--border-hairline)' }}>
      <div className="rh-news-grid" style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: 'clamp(34px, 5vh, 56px) var(--gutter)',
        display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 440px', gap: 'clamp(20px, 4vw, 56px)', alignItems: 'center' }}>
        <div>
          <div className="rh-eyebrow" style={{ color: 'var(--messing-dunkel)', marginBottom: 8 }}>{t('news.eyebrow')}</div>
          <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 'clamp(1.4rem, 1.1rem + 1vw, 2rem)', margin: 0, color: 'var(--text-strong)' }}>{t('news.title')}.</h2>
        </div>
        <div style={{ justifySelf: 'stretch', width: '100%' }}>
          {sent ? (
            <div style={{ display: 'flex', alignItems: 'center', gap: 16, textAlign: 'left', padding: '16px 20px',
              background: 'var(--surface-card)', border: '1px solid color-mix(in srgb, var(--messing) 42%, var(--border-hairline))',
              borderRadius: 'var(--radius-md)', boxShadow: 'var(--elev-2)' }}>
              {/* wax-seal check */}
              <span aria-hidden="true" style={{ display: 'inline-flex', flex: 'none', width: 46, height: 46, borderRadius: '50%',
                alignItems: 'center', justifyContent: 'center', color: 'var(--messing-dunkel)', fontSize: 'var(--fs-20)',
                background: 'radial-gradient(circle at 50% 36%, color-mix(in srgb, var(--messing) 20%, var(--surface-card)), var(--surface-card))',
                border: '1px solid var(--messing)',
                boxShadow: 'inset 0 0 0 3px color-mix(in srgb, var(--messing) 22%, transparent), 0 0 0 4px color-mix(in srgb, var(--messing) 10%, transparent)' }}>✓</span>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.1rem, 0.95rem + 0.5vw, 1.35rem)', color: 'var(--text-strong)', lineHeight: 1.2 }}>{t('news.sent_title')}</div>
                <p style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-13-5)', lineHeight: 1.55, color: 'var(--text-muted)', margin: '4px 0 0', maxWidth: '46ch' }}>{t('news.sent_body')}</p>
              </div>
            </div>
          ) : (
            <form onSubmit={(e) => { e.preventDefault(); if (emailOk && consent) setSent(true); }}>
              <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
                <input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder={t('news.placeholder')}
                  style={{ flex: '1 1 240px', minWidth: 0, fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-15)', padding: '12px 16px',
                    background: 'var(--surface-card)', border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-sm)', color: 'var(--text-strong)' }} />
                <button type="submit" disabled={!consent || !emailOk} style={{ flex: 'none', fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-15)', fontWeight: 500,
                  cursor: (consent && emailOk) ? 'pointer' : 'not-allowed', opacity: (consent && emailOk) ? 1 : 0.55,
                  padding: '12px 26px', background: 'var(--tinte)', color: 'var(--pergament-text)', border: '1px solid var(--tinte)', borderRadius: 'var(--radius-sm)' }}>
                  {t('news.button')}
                </button>
              </div>
              {email.trim() && !emailOk && (
                <p role="alert" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12-5)', lineHeight: 1.5, color: 'var(--text-muted)', margin: '8px 0 0' }}>{t('news.invalid')}</p>
              )}
              {/* Das Kästchen bleibt 15 px gross — die TREFFERFLÄCHE ist das ganze
                  Label (44 px hoch, Innenabstand statt Aussenabstand), damit der
                  Daumen den Satz trifft, nicht das Quadrat. */}
              <label style={{ display: 'flex', gap: 9, alignItems: 'flex-start', marginTop: 4, padding: '12px 0', minHeight: 44, cursor: 'pointer' }}>
                <input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)} required
                  style={{ marginTop: 2, accentColor: 'var(--messing)', width: 15, height: 15, flex: 'none' }} />
                <span style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12-5)', lineHeight: 1.5, color: 'var(--text-muted)' }}>
                  {t('news.consent')}{' '}
                  <a href="#" onClick={(e) => { e.preventDefault(); onNav && onNav('datenschutz'); }} style={{ color: 'var(--link)' }}>{t('news.privacy_more')}</a>
                </span>
              </label>
            </form>
          )}
        </div>
      </div>
    </section>
  );
}

function VerseBreak({ t }) {
  return (
    <section style={{ position: 'relative', overflow: 'hidden', background: 'var(--scrim-1)' }}>
      <img loading="lazy" decoding="async" src={window.RH_IMG.shepherds} alt="" aria-hidden="true"
        style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 28%', opacity: 0.42 }} />
      <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(20,18,14,0.72), rgba(20,18,14,0.82))' }} />
      <div aria-hidden="true" style={{ position: 'absolute', inset: 18, border: '1px solid rgba(156,124,67,0.3)', borderRadius: 'var(--radius-md)' }} />
      <div style={{ position: 'relative', maxWidth: '34ch', margin: '0 auto', textAlign: 'center', padding: 'clamp(44px, 6.5vw, 104px) var(--gutter)' }}>
        <span aria-hidden="true" style={{ color: 'var(--messing)', fontSize: 'var(--fs-19)', display: 'inline-block', marginBottom: 20 }}>❧</span>
        <p style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.5rem, 1.15rem + 1.2vw, 2.2rem)', lineHeight: 1.3, color: 'var(--pergament-text)', margin: 0, textWrap: 'balance' }}>
          „{t('scripture.verse')}“
        </p>
        <div className="rh-label" style={{ color: 'var(--messing)', marginTop: 18, justifyContent: 'center' }}>{t('scripture.ref')}</div>
      </div>
    </section>
  );
}

// Gentle placeholder pad — a soft, slowly-swelling organ-like chord (root, fifth,
// octave, tenth) so the "Klang" moment actually SOUNDS before real MP3s exist.
// Returns { stop() }. Replaced automatically once a track carries a real `src`.
function rhMakePad(root) {
  try {
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return { stop() {} };
    const ctx = new AC();
    const master = ctx.createGain();
    master.gain.setValueAtTime(0.0001, ctx.currentTime);
    master.gain.exponentialRampToValueAtTime(0.075, ctx.currentTime + 1.4);
    const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 1700;
    filter.connect(master); master.connect(ctx.destination);
    const freqs = [root, root * 1.5, root * 2, root * 2.5];
    const oscs = freqs.map((f, i) => {
      const o = ctx.createOscillator(); o.type = i % 2 ? 'sine' : 'triangle'; o.frequency.value = f;
      const g = ctx.createGain(); g.gain.value = 0.9 / (i + 1.5); o.connect(g); g.connect(filter); o.start(); return o;
    });
    return { stop() {
      try {
        master.gain.cancelScheduledValues(ctx.currentTime);
        master.gain.setValueAtTime(master.gain.value, ctx.currentTime);
        master.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.6);
        setTimeout(() => { oscs.forEach((o) => { try { o.stop(); } catch (e) {} }); try { ctx.close(); } catch (e) {} }, 700);
      } catch (e) {}
    } };
  } catch (e) { return { stop() {} }; }
}

function AudioListen({ t }) {
  const { SectionHeading } = homeNS;
  const tracks = (window.RH_AUDIO || []).filter((x) => !x.parked);
  const [active, setActive] = React.useState(-1);
  const [prog, setProg] = React.useState(0);
  const engineRef = React.useRef(null);
  const rafRef = React.useRef(0);
  // Für das Springen: der synthetische Antrieb rechnet die Position aus einem
  // Startzeitpunkt — der muss verschiebbar sein, sonst kann man nicht springen.
  const startRef = React.useRef(0);
  const audioRef = React.useRef(null);
  const totalRef = React.useRef(1);
  const parseLen = (s) => { const p = String(s || '0:0').split(':').map(Number); return (p[0] * 60) + (p[1] || 0); };
  const fmt = (sec) => { sec = Math.max(0, Math.floor(sec)); return Math.floor(sec / 60) + ':' + String(sec % 60).padStart(2, '0'); };
  const stopAll = React.useCallback(() => {
    if (engineRef.current) { try { engineRef.current.stop(); } catch (e) {} engineRef.current = null; }
    audioRef.current = null;
    cancelAnimationFrame(rafRef.current);
  }, []);
  React.useEffect(() => () => stopAll(), [stopAll]);
  const play = (i, abPosition) => {
    if (active === i && abPosition == null) { stopAll(); setActive(-1); setProg(0); return; }
    stopAll();
    const tr = tracks[i]; const total = parseLen(tr.len) || 1;
    totalRef.current = total;
    const ab = Math.max(0, Math.min(0.999, abPosition || 0));
    startRef.current = performance.now() - ab * total * 1000;
    setActive(i); setProg(ab);
    if (tr.src) {
      const a = new Audio(tr.src);
      a.currentTime = ab * total;
      a.play().catch(() => {});
      audioRef.current = a;
      engineRef.current = { stop() { try { a.pause(); } catch (e) {} } };
      const tick = () => { const d = a.duration || total; totalRef.current = d; setProg(Math.min(1, (a.currentTime || 0) / d)); if (a.ended) { setActive(-1); setProg(0); return; } rafRef.current = requestAnimationFrame(tick); };
      rafRef.current = requestAnimationFrame(tick);
    } else {
      audioRef.current = null;
      engineRef.current = rhMakePad(tr.root || 220);
      const tick = () => { const p = Math.min(1, ((performance.now() - startRef.current) / 1000) / totalRef.current); setProg(p); if (p >= 1) { stopAll(); setActive(-1); setProg(0); return; } rafRef.current = requestAnimationFrame(tick); };
      rafRef.current = requestAnimationFrame(tick);
    }
  };

  // Springen. Läuft gerade nichts, beginnt die Wiedergabe an der Stelle — das
  // ist, was ein Hörer erwartet, wenn er in eine Laufleiste klickt.
  const springen = (i, p) => {
    const ziel = Math.max(0, Math.min(1, p));
    if (active !== i) { play(i, ziel); return; }
    if (audioRef.current) { try { audioRef.current.currentTime = ziel * (audioRef.current.duration || totalRef.current); } catch (e) {} }
    else { startRef.current = performance.now() - ziel * totalRef.current * 1000; }
    setProg(ziel);
  };

  const feat = tracks[0]; const rest = tracks.slice(1);
  const featTotal = feat ? parseLen(feat.len) : 0;
  const featActive = active === 0;
  // Laufleiste: klicken springt, ziehen schrubbt, Pfeiltasten für die Feinarbeit.
  const leisteRef = React.useRef(null);
  const [schrubbt, setSchrubbt] = React.useState(false);
  const ausPunkt = (e) => {
    const el = leisteRef.current; if (!el) return;
    const k = el.getBoundingClientRect();
    springen(0, (e.clientX - k.left) / (k.width || 1));
  };
  const tastenSprung = (e) => {
    const jetzt = featActive ? prog : 0;
    const schritt = (e.shiftKey ? 30 : 5) / (featTotal || 1);   // 5 s, mit Shift 30 s
    const ziel = { ArrowLeft: jetzt - schritt, ArrowRight: jetzt + schritt, Home: 0, End: 0.999 }[e.key];
    if (ziel == null) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); play(0); } return; }
    e.preventDefault();
    springen(0, ziel);
  };
  const R = 78, C = 2 * Math.PI * R; // progress ring geometry
  // Hörproben-Zeilen: der Knopf IST die Uhr. Kein waagerechter Balken, der Platz
  // frisst — der Ring um den Kreis läuft ab wie ein Zeiger. Gleiche Idee wie beim
  // Feature-Player oben, nur klein. Geometrie: r so, dass die 1,5-px-Linie innen
  // liegt (44 px Kreis).
  const rR = 21.25, rC = 2 * Math.PI * rR;

  return (
    <section id="rh-klang" style={{ background: 'var(--nachtschiefer)', color: 'var(--pergament-text)' }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: 'clamp(64px, 8vw, 120px) var(--gutter)' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', gap: 24, marginBottom: 40, flexWrap: 'wrap' }}>
          <SectionHeading tone="dark" label={t('audio.eyebrow')} title={t('audio.title')} lead={t('audio.lead')} leadAll={window.RH_ALL('audio.lead')} lang={t.lang} />
          <a href={(window.RH_AUDIO_LINKS && window.RH_AUDIO_LINKS.spotify) || '#'} target="_blank" rel="noopener" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', fontWeight: 500, color: 'var(--messing)', border: '1px solid rgba(156,124,67,0.5)', padding: '10px 20px', borderRadius: 999, textDecoration: 'none' }}>
            <svg width="16" height="16" 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-.9 6.9-.5 9.5 1a.6.6 0 0 1 .3.9zm1.2-2.7a.8.8 0 0 1-1 .3c-2.6-1.6-6.6-2.1-9.7-1.1a.8.8 0 1 1-.5-1.5c3.5-1.1 7.9-.6 10.9 1.2.4.2.5.7.3 1.1zm.1-2.8C14.7 9 9.3 8.8 6.3 9.7a.9.9 0 1 1-.5-1.8c3.5-1 9.4-.8 13 1.3a.9.9 0 1 1-.9 1.6z" /></svg>
            {t('audio.spotify')}
          </a>
        </div>

        {/* Signature recording — the moment the site sounds. Organ medallion with a
            progress ring; a real player choreography that works on placeholder audio. */}
        {feat && (
          <div className="rh-audio-feature" style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: 'clamp(28px, 4vw, 56px)', alignItems: 'center',
            padding: 'clamp(26px, 3vw, 40px)', border: '1px solid var(--hairline-on-inverse)', borderRadius: 'var(--radius-md)',
            background: 'radial-gradient(120% 120% at 15% 20%, rgba(156,124,67,0.10), transparent 60%)', marginBottom: rest.length ? 22 : 0 }}>
            <div style={{ position: 'relative', width: 196, height: 196, flexShrink: 0, justifySelf: 'center' }}>
              <svg width="196" height="196" viewBox="0 0 196 196" style={{ position: 'absolute', inset: 0, transform: 'rotate(-90deg)' }} aria-hidden="true">
                <circle cx="98" cy="98" r={R} fill="none" stroke="rgba(156,124,67,0.22)" strokeWidth="2" />
                <circle cx="98" cy="98" r={R} fill="none" stroke="var(--messing)" strokeWidth="2.5" strokeLinecap="round"
                  strokeDasharray={C} strokeDashoffset={C * (1 - (featActive ? prog : 0))} style={{ transition: 'stroke-dashoffset 0.1s linear' }} />
              </svg>
              <div style={{ position: 'absolute', inset: 22, borderRadius: '50%', overflow: 'hidden', border: '1px solid rgba(156,124,67,0.35)' }}>
                <img loading="lazy" decoding="async" src={feat.img} alt="" aria-hidden="true" style={{ width: '100%', height: '100%', objectFit: 'cover', opacity: 0.92 }} />
                <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(circle at 50% 40%, rgba(18,16,12,0.1), rgba(18,16,12,0.5))' }} />
              </div>
              <button onClick={() => play(0)} aria-label={featActive ? 'Pause' : 'Play'} style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%,-50%)',
                width: 62, height: 62, borderRadius: '50%', border: 'none', background: 'var(--messing)', color: 'var(--nachtschiefer)', cursor: 'pointer',
                display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 'var(--fs-20)', boxShadow: 'var(--elev-2)' }}>
                {featActive ? '❚❚' : <span style={{ marginLeft: 3 }}>▶</span>}
              </button>
            </div>
            <div style={{ minWidth: 0 }}>
              <div className="rh-label" style={{ color: 'var(--text-on-inverse-muted)', marginBottom: 12 }}>{t.f({ de: 'Höreindruck', en: 'Listen', cs: 'Poslech' })}</div>
              <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'clamp(1.5rem, 1.2rem + 1vw, 2.1rem)', color: 'var(--pergament-text)', lineHeight: 1.2 }}>{t.f(feat.title)}</div>
              <div className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-14)', color: 'rgba(236,230,216,0.66)', marginTop: 6 }}>{feat.composer} · {t.f(feat.meta)}</div>
              <div style={{ marginTop: 22, display: 'flex', alignItems: 'center', gap: 14 }}>
                <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'rgba(236,230,216,0.6)', minWidth: 34 }}>{fmt(featActive ? prog * featTotal : 0)}</span>
                <div ref={leisteRef}
                  onPointerDown={(e) => { e.currentTarget.setPointerCapture(e.pointerId); setSchrubbt(true); ausPunkt(e); }}
                  onPointerMove={(e) => { if (schrubbt) ausPunkt(e); }}
                  onPointerUp={(e) => { setSchrubbt(false); try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (x) {} }}
                  onKeyDown={tastenSprung}
                  role="slider" tabIndex={0}
                  aria-label={t.f({ de: 'Abspielposition', en: 'Playback position', cs: 'Pozice přehrávání' })}
                  aria-valuemin={0} aria-valuemax={100}
                  aria-valuenow={Math.round((featActive ? prog : 0) * 100)}
                  aria-valuetext={fmt((featActive ? prog : 0) * featTotal) + ' / ' + feat.len}
                  style={{ flex: 1, minWidth: 0, position: 'relative', height: 24, display: 'flex', alignItems: 'center',
                    cursor: schrubbt ? 'grabbing' : 'pointer', touchAction: 'none' }}>
                  {/* Die Spur bleibt eine 3-px-Haarlinie wie im übrigen System, die
                      Trefferfläche ist 24 px hoch. Ein 3-px-Ziel wäre auf dem
                      Telefon nicht bedienbar. */}
                  <span aria-hidden="true" style={{ position: 'absolute', left: 0, right: 0, height: 3, background: 'rgba(156,124,67,0.22)', borderRadius: 'var(--radius-sm)' }} />
                  <span aria-hidden="true" style={{ position: 'absolute', left: 0, width: (featActive ? prog * 100 : 0) + '%', height: 3, background: 'var(--messing)', borderRadius: 'var(--radius-sm)', transition: schrubbt ? 'none' : 'width 0.1s linear' }} />
                  {/* Der Griff erscheint beim Anfassen und bei Tastaturfokus —
                      sonst bleibt die Leiste ruhig. */}
                  <span aria-hidden="true" style={{ position: 'absolute', left: (featActive ? prog * 100 : 0) + '%',
                    width: 11, height: 11, marginLeft: -5.5, borderRadius: '50%', background: 'var(--messing)',
                    boxShadow: '0 0 0 3px rgba(26,24,19,0.55)',
                    opacity: schrubbt ? 1 : 0, transform: schrubbt ? 'scale(1)' : 'scale(0.6)',
                    transition: 'opacity var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-standard)' }} />
                </div>
                <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'rgba(236,230,216,0.6)', minWidth: 34, textAlign: 'right' }}>{feat.len}</span>
              </div>
              {!feat.src && (
                <div style={{ marginTop: 14, fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-12-5)', color: 'rgba(236,230,216,0.5)', letterSpacing: '0.01em' }}>
                  {t.f({ de: 'Klangprobe · die vollständige Aufnahme folgt.', en: 'Sound sample · the full recording will follow.', cs: 'Zvuková ukázka · plná nahrávka bude následovat.' })}
                </div>
              )}
            </div>
          </div>
        )}

        {rest.length > 0 && (
          <div style={{ border: '1px solid var(--hairline-on-inverse)', borderRadius: 'var(--radius-md)', overflow: 'hidden' }}>
            {rest.map((tr, k) => {
              const i = k + 1; const isOn = active === i; const total = parseLen(tr.len);
              return (
                <div key={i} style={{ display: 'grid', gridTemplateColumns: '48px 1fr auto', gap: 18, alignItems: 'center', padding: '14px 20px', borderBottom: k < rest.length - 1 ? '1px solid var(--hairline-on-inverse)' : 'none', background: isOn ? 'rgba(156,124,67,0.10)' : 'transparent', transition: 'background 220ms var(--ease-out)' }}>
                  <span style={{ position: 'relative', width: 44, height: 44, display: 'block' }}>
                    <svg width="44" height="44" viewBox="0 0 44 44" aria-hidden="true" style={{ position: 'absolute', inset: 0, transform: 'rotate(-90deg)', pointerEvents: 'none' }}>
                      <circle cx="22" cy="22" r={rR} fill="none" stroke="rgba(156,124,67,0.34)" strokeWidth="1.5" />
                      <circle cx="22" cy="22" r={rR} fill="none" stroke="var(--messing)" strokeWidth="1.5" strokeLinecap="round"
                        strokeDasharray={rC} strokeDashoffset={rC * (1 - (isOn ? prog : 0))} style={{ transition: 'stroke-dashoffset 0.1s linear' }} />
                    </svg>
                    <button onClick={() => play(i)} aria-label={isOn ? 'Pause' : 'Play'}
                      style={{ position: 'absolute', inset: 0, width: 44, height: 44, borderRadius: '50%', border: 'none', background: 'none',
                        color: 'var(--messing)', cursor: 'pointer', fontSize: isOn ? 12 : 13, display: 'grid', placeItems: 'center', padding: 0 }}>
                      {isOn ? '❚❚' : <span style={{ marginLeft: 2 }}>▶</span>}
                    </button>
                  </span>
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 500, fontSize: 'var(--fs-20)', color: 'var(--pergament-text)' }}>{t.f(tr.title)}</div>
                    <div className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: 'rgba(236,230,216,0.6)', marginTop: 3, lineHeight: 1.45 }}>{tr.composer} · {t.f(tr.meta)}</div>
                  </div>
                  {/* Feste Breite: die Zeit wechselt zwischen Länge und Restlauf, ohne die
                      Spalte zu bewegen — in jeder Sprache dieselbe Geometrie. */}
                  <span className="rh-num" style={{ fontFamily: 'var(--font-ui)', fontSize: 'var(--fs-13)', color: isOn ? 'var(--messing)' : 'rgba(236,230,216,0.6)', minWidth: 38, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>{isOn ? fmt(prog * total) : tr.len}</span>
                </div>
              );
            })}
          </div>
        )}
      </div>
    </section>
  );
}

function Kantorat({ t, onNav }) {
  const { Button, ImagePlate, SectionHeading } = homeNS;
  return (
    <section style={{ position: 'relative', overflow: 'hidden', background: 'var(--scrim-1)' }}>
      {(() => {
        const s = window.RH_slot ? window.RH_slot('home-atelier', { src: window.RH_IMG.positivRoom, imgPosition: 'center 42%' }) : null;
        const src = s ? s.src : window.RH_IMG.positivRoom;
        const op = s && s.imgPosition ? s.imgPosition : 'center 42%';
        const zoom = s && s.zoom ? s.zoom : 1;
        return (
          <img loading="lazy" decoding="async" src={src} alt="" aria-hidden="true" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: op, transform: zoom !== 1 ? 'scale(' + zoom + ')' : undefined, transformOrigin: op, opacity: 0.42 }} />
        );
      })()}
      <div className="rh-kantorat-scrim" style={{ position: 'absolute', inset: 0, background: 'linear-gradient(96deg, rgba(18,16,12,0.94), rgba(18,16,12,0.62) 62%, rgba(18,16,12,0.34))' }} />
      <div className="rh-kantorat-frame" aria-hidden="true" style={{ position: 'absolute', inset: 18, border: '1px solid rgba(156,124,67,0.3)', borderRadius: 'var(--radius-md)' }} />
      <div className="rh-kantorat-grid" style={{ position: 'relative', maxWidth: 'var(--container)', margin: '0 auto', padding: 'clamp(64px, 8vw, 120px) var(--gutter)', display: 'grid', gridTemplateColumns: '1.15fr 0.85fr', gap: 'clamp(40px, 6vw, 88px)', alignItems: 'start' }}>
        <div style={{ maxWidth: '46ch' }}>
          <SectionHeading tone="dark" label={t.f({ de: 'Das Herz', en: 'The heart', cs: 'Srdce' })} title={t.f({ de: 'Wo die Musik entsteht', en: 'Where the music begins', cs: 'Kde hudba vzniká' })} />
          {window.RH_ParityText({ as: 'p', lang: t.lang, style: { marginTop: 22 }, textStyle: { fontFamily: 'var(--font-body)', fontSize: 'clamp(1.05rem, 1rem + 0.4vw, 1.3rem)', lineHeight: 1.75, color: 'rgba(236,230,216,0.88)' }, all: { de: 'In seiner Dresdner Wohnung stehen Cembalo und Orgelpositiv — der Ort täglicher Übung. Hier entstehen die Programme, die er an den historischen Orgeln des Osterzgebirges und Böhmens spielt.', en: 'In his Dresden flat stand the harpsichord and the positive organ — the place of daily practice. Here the programmes take shape that he plays on the historic organs of the Ore Mountains and Bohemia.', cs: 'V jeho drážďanském bytě stojí cembalo a varhanní pozitiv — místo každodenního cvičení. Zde vznikají programy, které pak hraje na historických varhanách Krušnohoří a Čech.' } })}
          <div style={{ marginTop: 30, paddingTop: 24, borderTop: '1px solid rgba(156,124,67,0.28)' }}>
            <div className="rh-label" style={{ color: 'var(--text-on-inverse-muted)', marginBottom: 10 }}>{t.f({ de: 'Im Dienst der Gemeinde', en: 'In service of the parish', cs: 'Ve službě farnosti' })}</div>
            <p style={{ fontFamily: 'var(--font-body)', fontSize: 'clamp(1rem, 0.96rem + 0.3vw, 1.15rem)', lineHeight: 1.7, color: 'rgba(236,230,216,0.78)', margin: 0, maxWidth: '46ch' }}>{t.f({ de: 'Als Kantor im Ev.-Luth. Kirchspiel Osterzgebirge - Region Altenberg gestaltet Roy Heyne die Orgel, Chor- und Instrumentalmusik im Gottesdienst und zu musikalischen Veranstaltungen — mit den musikalischen Gruppen der Kantorei Altenberg.', en: 'As cantor of the Ev. Lutheran Parish of Osterzgebirge - Region Altenberg, Roy Heyne shapes the organ, choral and instrumental music in worship and at musical events — with the ensembles of the Kantorei Altenberg.', cs: 'Jako kantor ev.-luter. farnosti Osterzgebirge - Region Altenberg utváří Roy Heyne varhanní, sborovou a instrumentální hudbu při bohoslužbách a hudebních akcích — s hudebními skupinami sboru Kantorei Altenberg.' })}</p>
            <div style={{ marginTop: 18 }}>
              <Button variant="ghost" onClick={() => onNav && onNav('konzerte', 'dienste')} style={{ color: 'var(--pergament-text)' }} iconAfter={<span>→</span>}>{t.f({ de: 'Gottesdienste ansehen', en: 'View services', cs: 'Zobrazit bohoslužby' })}</Button>
            </div>
          </div>
        </div>
        <div className="rh-kantorat-plate">
          <ImagePlate src={window.RH_IMG.positivFront} slot="home-atelier-plate" ratio="4 / 5" arch tone="dark"
            caption={t.f({ de: 'Orgelpositiv · Dresden', en: 'Bohemian positive organ · Dresden', cs: 'České varhanní pozitiv · Drážďany' })} />
        </div>
      </div>
    </section>
  );
}

function Home({ onNav, t }) {
  // Erzählbogen (nach der zweiten Apple-Runde — verdichtet auf acht Sektionen):
  // Ehrfurcht (Hero) → wann/wo + Flaggschiff-Band Orgelsommer (Konzerte) → JETZT
  // HÖREN (Klang, Position 3) → den Menschen treffen (Porträt) → das Dresdner
  // Atelier (Kantorat) → Wegweiser (Vita / Instrumente / Edition) → geistliche Coda
  // (Vers) → schlanke Newsletter-Anmeldung im Fußbereich.
  // Der Orgelsommer ist aus der eigenen Vollsektion in die Konzerte gefaltet
  // (Flaggschiff-Band über den Einzelterminen), der Newsletter auf eine Zeile
  // reduziert. Reale Tonfolge: D·L·D·L·D·L·D·L — saubere Alternation, kein Klumpen.
  return (
    <div>
      <Hero onNav={onNav} t={t} />
      <UpcomingConcerts onNav={onNav} t={t} />
      <AudioListen t={t} />
      <Portrait t={t} />
      <Kantorat t={t} onNav={onNav} />
      <Teasers onNav={onNav} t={t} />
      <VerseBreak t={t} />
      <Newsletter t={t} onNav={onNav} />
    </div>
  );
}

Object.assign(window, { Home, rhMakePad });
