// ─── Livescore ──────────────────────────────────────────────────
// Vollbild-Platzbelegung fuers Turnier — gedacht zum Spiegeln auf den
// 75"-TV (Barco ClickShare). Hintergrund ist das Luftbild der Anlage,
// darueber liegen 6 Labels (Platz 1..6) mit der aktuell laufenden Partie.
//
// Quelle der Belegung: Singleton-Tabelle `display_board` (jsonb-Array mit
// 6 Slots, je { a, b } = zwei registration-IDs). Anon liest (TV ohne
// Login), nur Admin schreibt (RLS). Realtime pusht jede Aenderung sofort
// auf den TV. Angezeigt wird live aufgeloest aus dem registrations-State,
// damit Namens-Korrekturen automatisch durchschlagen.
//
// Seite ist BEWUSST versteckt: nicht in NAV/LEGAL_NAV, nur ueber
// #livescore erreichbar (HIDDEN_PAGES in index.html).

const { useState, useEffect, useMemo } = React;

// Prozent-Positionen der 6 Platz-Labels auf dem Luftbild. Reihenfolge
// folgt der Vorgabe: oben rechts = Platz 1, dann nach links Platz 2/3;
// unten rechts = Platz 4, dann Platz 5/6. Feinjustage per Screenshot.
const COURT_POS = [
  { x: 71.3, y: 26.5 }, // Platz 1 — oben rechts (separater Platz an den Gebaeuden)
  { x: 52, y: 29 }, // Platz 2 — oben Mitte (rechte Haelfte des 2er-Blocks)
  { x: 38.2, y: 29 }, // Platz 3 — oben links (linke Haelfte des 2er-Blocks)
  { x: 72, y: 70.6 }, // Platz 4 — unten rechts
  { x: 57.7, y: 70.6 }, // Platz 5 — unten Mitte
  { x: 43.5, y: 70.6 }, // Platz 6 — unten links
];

const EMPTY_BOARD = COURT_POS.map(() => ({ a: null, b: null }));

// Mobile (≤700px): das Luftbild wird per CSS auf den Platz-Bereich
// gezoomt (Wasser/Umfeld weg → Plaetze gross). M_CROP hier und die
// aspect-ratio/background-Werte im ≤700-Block von styles.css
// beschreiben DENSELBEN Ausschnitt — bei Aenderung beide anpassen!
// Karten sitzen auf ihren (jetzt grossen) Plaetzen: x = echte
// Platzmitte (auf den Crop umgerechnet), y leicht im Zickzack, damit
// sich benachbarte Karten auch mit vollem Score nie beruehren
// (per Pillow-Mock fuer 360/393px verifiziert).
const M_CROP = { x: 26, y: 4, w: 58, h: 90 }; // % des Originalbilds
const M_CARD_Y = [15, 15, 39, 65, 85, 65];    // Karten-y, Platz 1..6
const COURT_POS_M = COURT_POS.map((c, i) => ({
  x: (c.x - M_CROP.x) / M_CROP.w * 100,
  y: M_CARD_Y[i],
}));

// Sponsoren fuer den linken Seitenbalken (neben dem quadratischen
// Luftbild). Auf 16:9-TVs entsteht seitlich Platz — dort laeuft die
// Sponsor-Rotation. Hintergrund ist BEIGE (wie die Website); Logos liegen
// TRANSPARENT und FARBIG drauf — siehe `.spo-logo` in styles.css. EDEKA ist
// hell-auf-transparent → braucht als einziges eine dunkle Kachel (`dark`).
// `sub` = Zeile unter dem Logo (nur wo gewuenscht, sonst Logo allein).
// `round` = Logo bringt eigenen Vollflaechen-Hintergrund mit (GMW) und
// bekommt nur abgerundete Ecken wie die EDEKA-Kachel. `compact` =
// kleinere Darstellung (Scaly: nur 393px Quelldatei, wird bei voller
// Breite unscharf und der Schriftzug wirkt ueberdimensioniert).
const SPONSORS = [
  { src: 'uploads/edeka-badge.png', alt: 'EDEKA Riasanow', dark: true },
  { src: 'uploads/hoerl_neu.png',   alt: 'Getränke Hörl', sub: 'Claudia Huber' },
  { src: 'uploads/vr.png',          alt: 'VR Bank Bayern Mitte' },
  { src: 'uploads/GMW_grau.png',    alt: 'GMW Studio', round: true },
  { src: 'uploads/allfinanz.png',   alt: 'Allfinanz · Roland Pietsch', sub: 'Roland Pietsch' },
  { src: 'uploads/fleischer.png',   alt: 'Fahrschule Fleischer' },
  { src: 'uploads/schanzer.svg',    alt: 'Schanzer Besaitungsservice' },
  { src: 'uploads/scaly.png',       alt: 'Scaly Academy', compact: true },
];

// Sponsoren-Bande (links): zeigt immer EINEN Sponsor gross, wie eine
// Broadcast-Einblendung im TV. Die Folie faehrt mit Fade + leichtem Zoom
// ein, haelt und gleitet wieder aus (CSS `spo-cycle`; key-Remount startet
// die Animation pro Wechsel neu, animation-duration inline = SPO_SLOT,
// damit CSS und JS-Takt synchron bleiben). Die `sub`-Zeile sitzt IN der
// Logo-Box direkt unterm Logo.
const SPO_SLOT = 5000; // ms pro Sponsor

function SponsorRail() {
  const [spoIdx, setSpoIdx] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setSpoIdx((i) => (i + 1) % SPONSORS.length), SPO_SLOT);
    return () => clearInterval(t);
  }, []);
  const s = SPONSORS[spoIdx];
  return (
    <div className="board-rail board-rail-left">
      <div className="board-rail-title"><span>Unsere Partner</span></div>
      <div className="spo-stage">
        <div className="spo-slide" key={spoIdx} style={{ animationDuration: SPO_SLOT + 'ms' }}>
          <div className={`spo-logo ${s.dark ? 'is-dark' : ''} ${s.round ? 'is-round' : ''} ${s.compact ? 'is-compact' : ''}`}>
            <img src={s.src} alt={s.alt} />
            {s.sub ? <div className="spo-name">{s.sub}</div> : null}
          </div>
        </div>
      </div>
    </div>
  );
}

// Kurzform fuers Scoreboard: nur Nachnamen ("Müller / Vanik").
function shortPair(reg) {
  if (!reg) return '—';
  const [a, b] = orderedPair(reg);
  return `${a.last} / ${b.last}`;
}

// ─── "Als Nächstes" (rechter Balken) ────────────────────────────
// Die Scores stehen auf den Platz-Karten im Bild — rechts laufen die
// WARTENDEN Partien: automatisch aus dem Tableau (beide Teams stehen
// fest, kein Sieger, nicht gerade auf einem Platz) plus manuelle
// Admin-Einträge aus display_board.next. `time` = Tableau-Format
// "Sa 09:00"; ohne Zeit ans Ende. Dedup über die Team-Paarung, manuelle
// Einträge gewinnen (stabiler Sort hält sie bei gleicher Zeit vorn).
const NX_DAY = { Fr: 0, Sa: 1, So: 2 };
function nxTimeKey(t) {
  const m = /^(Fr|Sa|So) (\d{2}):(\d{2})$/.exec(t || '');
  if (!m) return Infinity;
  return NX_DAY[m[1]] * 10000 + parseInt(m[2], 10) * 100 + parseInt(m[3], 10);
}
const nxPairKey = (a, b) => [a, b].sort().join('|');

function upcomingMatches(bracket, courts, next) {
  const manual = (next && next.manual) || [];
  const hidden = new Set((next && next.hidden) || []);
  const busy = new Set();
  (courts || []).forEach((c) => { if (c && c.a && c.b) busy.add(nxPairKey(c.a, c.b)); });
  const out = [];
  const seen = new Set();
  const push = (id, a, b, time, court, isManual) => {
    const key = nxPairKey(a, b);
    if (busy.has(key) || seen.has(key)) return;
    seen.add(key);
    out.push({ id, a, b, time: time || null, court: court || null, manual: !!isManual });
  };
  manual.forEach((e) => { if (e && e.a && e.b) push(e.id, e.a, e.b, e.time, e.court, true); });
  const rounds = [...((bracket && bracket.main) || []), ...((bracket && bracket.cons) || [])];
  rounds.forEach((round) => round.forEach((m) => {
    if (m && m.p1 && m.p2 && matchWinnerIdx(m) === null && !hidden.has(m.id)) {
      push(m.id, m.p1, m.p2, m.time, m.court, false);
    }
  }));
  out.sort((x, y) => nxTimeKey(x.time) - nxTimeKey(y.time));
  return out.slice(0, 6);
}

// Rechter Balken: bewusst ruhig — pro Karte eine Kopfzeile (Zeit hell,
// Platz gold) und die beiden Teams. Admin pflegt manuelle Einträge über
// den Stift neben dem Titel.
function NextRail({ matches, regById, isAdmin, onEdit }) {
  return (
    <div className="board-rail board-rail-right">
      <div className="board-rail-title">
        <span>Als Nächstes</span>
        {isAdmin ? (
          <button type="button" className="nx-edit" onClick={onEdit} aria-label="Nächste Spiele bearbeiten">
            <Icon name="edit" size={13} />
          </button>
        ) : null}
      </div>
      {matches.length === 0 ? (
        <div className="nx-empty">Spielplan folgt</div>
      ) : (
        <div className="nx-list">
          {matches.map((m) => {
            const ra = regById[m.a];
            const rb = regById[m.b];
            const Team = ({ reg }) => (
              <div className="nx-team">
                <span className="nx-name">{shortPair(reg)}</span>
                {reg && pairClubs(reg) ? <span className="nx-club">{pairClubs(reg)}</span> : null}
              </div>
            );
            return (
              <div className="nx-card" key={m.id}>
                <div className="nx-head">
                  <span className="nx-time">{m.time || 'offen'}</span>
                  {m.court ? <span className="nx-court">{m.court}</span> : null}
                </div>
                <Team reg={ra} />
                <div className="nx-vs"><span>vs</span></div>
                <Team reg={rb} />
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// Admin-Modal fuer "Als Nächstes": ALLE angezeigten Partien lassen sich
// einzeln bearbeiten oder entfernen — manuelle direkt; automatische aus
// dem Tableau werden beim Entfernen ausgeblendet (hidden, unten
// wiederherstellbar) bzw. beim Bearbeiten durch einen manuellen Eintrag
// ersetzt. Jede Aktion speichert sofort — Realtime verteilt an alle
// Clients. Das Tableau selbst bleibt unangetastet.
function NextModal({ nextObj, upcoming, bracket, registrations, regById, onSave, onClose }) {
  const [a, setA] = useState('');
  const [b, setB] = useState('');
  const [day, setDay] = useState('');
  const [hour, setHour] = useState('');
  const [court, setCourt] = useState('');
  const [editing, setEditing] = useState(null); // { type: 'manual'|'auto', id }

  const manual = nextObj.manual || [];
  const hidden = nextObj.hidden || [];

  const teamOptions = useMemo(() => {
    return [...registrations]
      .map((r) => ({ id: r.id, label: `${pairLabel(r)}${teamClub(r) ? ' · ' + teamClub(r) : ''}` }))
      .sort((x, y) => x.label.localeCompare(y.label, 'de'));
  }, [registrations]);

  const teamText = (id) => {
    const r = id ? regById[id] : null;
    return r ? pairLabel(r) : '—';
  };

  const reset = () => { setA(''); setB(''); setDay(''); setHour(''); setCourt(''); setEditing(null); };

  const startEdit = (m) => {
    setEditing({ type: m.manual ? 'manual' : 'auto', id: m.id });
    setA(m.a); setB(m.b);
    const t = /^(Fr|Sa|So) (\d{2}:\d{2})$/.exec(m.time || '');
    setDay(t ? t[1] : ''); setHour(t ? t[2] : '');
    setCourt(m.court || '');
  };

  const removeEntry = (m) => {
    if (m.manual) onSave({ manual: manual.filter((e) => e.id !== m.id), hidden });
    else onSave({ manual, hidden: [...hidden, m.id] });
    if (editing && editing.id === m.id) reset();
  };

  const restore = (id) => onSave({ manual, hidden: hidden.filter((h) => h !== id) });

  const canSave = a && b && a !== b;
  const submit = () => {
    if (!canSave) return;
    const entry = {
      id: editing && editing.type === 'manual' ? editing.id : 'n' + Date.now(),
      a, b,
      time: day && hour ? `${day} ${hour}` : null,
      court: court || null,
    };
    if (editing && editing.type === 'manual') {
      onSave({ manual: manual.map((e) => (e.id === editing.id ? entry : e)), hidden });
    } else if (editing && editing.type === 'auto') {
      // Original ausblenden, Override als manueller Eintrag speichern.
      onSave({ manual: [...manual, entry], hidden: hidden.includes(editing.id) ? hidden : [...hidden, editing.id] });
    } else {
      onSave({ manual: [...manual, entry], hidden });
    }
    reset();
  };

  // Ausgeblendete Tableau-Partien (nur noch offene) zum Wiederherstellen.
  const hiddenMatches = useMemo(() => {
    const rounds = [...((bracket && bracket.main) || []), ...((bracket && bracket.cons) || [])];
    const out = [];
    rounds.forEach((round) => round.forEach((m) => {
      if (m && hidden.includes(m.id) && m.p1 && m.p2 && matchWinnerIdx(m) === null) out.push(m);
    }));
    return out;
  }, [bracket, hidden]);

  return (
    <div className="modal-overlay">
      <div className="modal board-modal" onClick={(e) => e.stopPropagation()}>
        <button type="button" className="modal-close" onClick={onClose} aria-label="Schließen"><Icon name="x" size={18} /></button>
        <h3>Nächste Spiele</h3>
        <p className="sub">Partien mit Zeit und Platz kommen automatisch aus dem Tableau. Jeder Eintrag lässt sich bearbeiten oder entfernen, manuelle Einträge kommen dazu.</p>

        {upcoming.length > 0 && (
          <div className="nx-man-list">
            {upcoming.map((m) => (
              <div className={`nx-man-item ${editing && editing.id === m.id ? 'is-editing' : ''}`} key={m.id}>
                <span className="nx-man-meta">{m.time || 'offen'}{m.court ? ' · ' + m.court : ''}</span>
                <span className="nx-man-teams">
                  {teamText(m.a)} <em>gegen</em> {teamText(m.b)}
                </span>
                <button type="button" className="nx-man-btn" onClick={() => startEdit(m)} aria-label="Bearbeiten"><Icon name="edit" size={13} /></button>
                <button type="button" className="nx-man-btn nx-man-del" onClick={() => removeEntry(m)} aria-label="Entfernen"><Icon name="x" size={14} /></button>
              </div>
            ))}
          </div>
        )}

        <div className="field">
          <label className="field-label">Team A</label>
          <select className="input" value={a} onChange={(e) => setA(e.target.value)}>
            <option value="">— wählen —</option>
            {teamOptions.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        </div>
        <div className="field">
          <label className="field-label">Team B</label>
          <select className="input" value={b} onChange={(e) => setB(e.target.value)}>
            <option value="">— wählen —</option>
            {teamOptions.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        </div>
        <div className="nx-form-row">
          <div className="field">
            <label className="field-label">Tag</label>
            <select className="input" value={day} onChange={(e) => setDay(e.target.value)}>
              <option value="">—</option>
              {DAY_OPTIONS.map((d) => <option key={d} value={d}>{d}</option>)}
            </select>
          </div>
          <div className="field">
            <label className="field-label">Uhrzeit</label>
            <select className="input" value={hour} onChange={(e) => setHour(e.target.value)}>
              <option value="">—</option>
              {HOUR_OPTIONS.map((h) => <option key={h} value={h}>{h}</option>)}
            </select>
          </div>
          <div className="field">
            <label className="field-label">Platz</label>
            <select className="input" value={court} onChange={(e) => setCourt(e.target.value)}>
              <option value="">—</option>
              {COURT_OPTIONS.map((c) => <option key={c} value={c}>{c}</option>)}
            </select>
          </div>
        </div>

        {hiddenMatches.length > 0 && (
          <div className="nx-hidden">
            <div className="board-quick-title">Ausgeblendet</div>
            {hiddenMatches.map((m) => (
              <div className="nx-man-item" key={m.id}>
                <span className="nx-man-teams">
                  {teamText(m.p1)} <em>gegen</em> {teamText(m.p2)}
                </span>
                <button type="button" className="btn btn-ghost nx-restore" onClick={() => restore(m.id)}>Wiederherstellen</button>
              </div>
            ))}
          </div>
        )}

        <div className="modal-actions board-modal-actions">
          {editing ? <button type="button" className="btn btn-ghost" onClick={reset}>Verwerfen</button> : null}
          <div className="spacer" />
          <button type="button" className="btn btn-ghost" onClick={onClose}>Schließen</button>
          <button type="button" className="btn btn-primary" onClick={submit} disabled={!canSave}>{editing ? 'Speichern' : 'Hinzufügen'}</button>
        </div>
      </div>
    </div>
  );
}

// Aus dem Bracket alle gerade spielbaren Partien ziehen (beide Teams
// gesetzt, noch kein Sieger) — als Schnellauswahl im Belegen-Modal.
function activeMatches(bracket) {
  if (!bracket) return [];
  const out = [];
  const buckets = [
    { key: 'main', label: 'HR', rounds: bracket.main || [] },
    { key: 'cons', label: 'TR', rounds: bracket.cons || [] },
  ];
  for (const bk of buckets) {
    bk.rounds.forEach((round, ri) => {
      round.forEach((m) => {
        // matchWinnerIdx statt !m.winner — winner=0 (Paar 1) ist auch fertig.
        if (m && m.p1 && m.p2 && matchWinnerIdx(m) === null) {
          out.push({ a: m.p1, b: m.p2, label: `${bk.label} R${ri + 1}`, court: m.court || '' });
        }
      });
    });
  }
  return out;
}

// Platz-Karte im Bild: Paarung + Live-Score. Sobald ein Ergebnis
// eingetragen ist (slot.sets), ersetzen Satz-Spalten die vs-Pille:
// obere Zeile = oberes Team, laufender Satz gold — die Namen bleiben
// unangetastet, die Karte waechst nur nach oben/unten (mittig verankert).
// Im Admin-Modus oeffnet ein Klick auf die Karte das Belegen-Modal
// (Platz zuweisen + Spielstand) — der rechte Balken zeigt nur noch
// "Als Nächstes".
function CourtLabel({ index, slot, regById, isAdmin, onEdit, mobile }) {
  const pos = mobile ? COURT_POS_M[index] : COURT_POS[index];
  const regA = slot && slot.a ? regById[slot.a] : null;
  const regB = slot && slot.b ? regById[slot.b] : null;
  const occupied = regA || regB;
  const sets = (slot && Array.isArray(slot.sets)) ? slot.sets : [];
  const last = sets.length - 1;

  // Nur Nachnamen auf den Platzkarten (kein Vornamen-Initial) — kompakter.
  // Jeder Spieler fix in eigener Zeile (Schraegstrich bei Spieler 1),
  // statt vom zufaelligen Umbruch abzuhaengen.
  const Team = ({ reg }) => {
    const pair = reg ? orderedPair(reg) : null;
    return (
      <div className="court-team">
        <div className="court-names">
          {pair ? (
            <>
              <span className="cn-line">{pair[0].last} /</span>
              <span className="cn-line">{pair[1].last}</span>
            </>
          ) : '—'}
        </div>
      </div>
    );
  };

  const SetRow = ({ side }) => (
    <div className="cs-row">
      {sets.map((st, si) => (
        <span key={si} className={`cs-cell ${si === last ? 'cur' : ''}`}>{st[side]}</span>
      ))}
    </div>
  );

  return (
    <div className={`court-label ${occupied ? 'is-on' : 'is-free'} ${isAdmin ? 'is-admin' : ''}`}
         style={{ left: pos.x + '%', top: pos.y + '%' }}
         onClick={isAdmin ? () => onEdit(index) : undefined}>
      <div className="court-head">Platz {index + 1}{occupied ? <span className="court-live" /> : null}</div>
      <div className="court-body">
        {occupied ? (
          <>
            <Team reg={regA} />
            {sets.length > 0 ? (
              <div className="court-sets">
                <SetRow side={0} />
                <div className="cs-div" />
                <SetRow side={1} />
              </div>
            ) : (
              <div className="court-vs"><span>vs</span></div>
            )}
            <Team reg={regB} />
          </>
        ) : (
          <div className="court-empty">frei</div>
        )}
      </div>
    </div>
  );
}

function BoardModal({ index, slot, registrations, bracket, regById, onSave, onClose }) {
  const [a, setA] = useState(slot ? (slot.a || '') : '');
  const [b, setB] = useState(slot ? (slot.b || '') : '');

  // Live-Spielstand: 3 Satz-Zeilen (Satz 3 = Match-Tiebreak) als Strings.
  const [sets, setSets] = useState(() => {
    const s = (slot && Array.isArray(slot.sets)) ? slot.sets : [];
    return [0, 1, 2].map((i) => ({
      a: s[i] && s[i][0] != null ? String(s[i][0]) : '',
      b: s[i] && s[i][1] != null ? String(s[i][1]) : '',
    }));
  });
  const updateSet = (i, side, val) =>
    setSets((prev) => prev.map((s, idx) => (idx === i ? { ...s, [side]: val } : s)));
  const emptySets = () => [{ a: '', b: '' }, { a: '', b: '' }, { a: '', b: '' }];
  const clearAll = () => { setA(''); setB(''); setSets(emptySets()); };
  // Team gewechselt -> alter Spielstand gilt nicht mehr, Score-Felder leeren.
  // (Bei gleichem Team feuert onChange nicht, Live-Score bleibt erhalten.)
  const pickA = (val) => { setA(val); setSets(emptySets()); };
  const pickB = (val) => { setB(val); setSets(emptySets()); };
  // Nur die tatsaechlich gefuellten Saetze (bis zum letzten mit Eingabe).
  const buildSets = () => {
    let last = -1;
    sets.forEach((s, i) => { if (s.a !== '' || s.b !== '') last = i; });
    const out = [];
    for (let i = 0; i <= last; i++) {
      out.push([parseInt(sets[i].a, 10) || 0, parseInt(sets[i].b, 10) || 0]);
    }
    return out;
  };

  // Teams alphabetisch nach erstem Nachnamen fuer die Dropdowns.
  const teamOptions = useMemo(() => {
    return [...registrations]
      .map((r) => ({ id: r.id, label: `${pairLabel(r)}${teamClub(r) ? ' · ' + teamClub(r) : ''}` }))
      .sort((x, y) => x.label.localeCompare(y.label, 'de'));
  }, [registrations]);

  const quick = useMemo(() => activeMatches(bracket), [bracket]);

  const teamText = (id) => {
    const r = id ? regById[id] : null;
    return r ? pairLabel(r) : '—';
  };

  return (
    <div className="modal-overlay">
      <div className="modal board-modal" onClick={(e) => e.stopPropagation()}>
        <button type="button" className="modal-close" onClick={onClose} aria-label="Schließen"><Icon name="x" size={18} /></button>
        <h3>Platz {index + 1} belegen</h3>

        {quick.length > 0 && (
          <div className="board-quick">
            <div className="board-quick-title">Schnellauswahl aus dem Tableau</div>
            <div className="board-quick-list">
              {quick.map((m, i) => (
                <button type="button" key={i} className="board-quick-item"
                        onClick={() => { setA(m.a); setB(m.b); setSets(emptySets()); }}>
                  <span className="bq-tag">{m.label}{m.court ? ' · ' + m.court : ''}</span>
                  <span className="bq-teams">{teamText(m.a)} <em>gegen</em> {teamText(m.b)}</span>
                </button>
              ))}
            </div>
          </div>
        )}

        <div className="field">
          <label className="field-label">Team A</label>
          <select className="input" value={a} onChange={(e) => pickA(e.target.value)}>
            <option value="">— frei —</option>
            {teamOptions.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        </div>
        <div className="field">
          <label className="field-label">Team B</label>
          <select className="input" value={b} onChange={(e) => pickB(e.target.value)}>
            <option value="">— frei —</option>
            {teamOptions.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        </div>

        {(a || b) && (
          <div className="field">
            <label className="field-label">Spielstand (live) — erscheint im Board rechts</label>
            {/* Eine Zeile pro Team (Name links, gut lesbar), Spalten = Sätze. */}
            <div className="board-score">
              <div className="bs-row bs-head">
                <span className="bs-team" />
                <span>Satz 1</span><span>Satz 2</span><span>MTB</span>
              </div>
              <div className="bs-row">
                <span className="bs-team">{teamText(a)}</span>
                {[0, 1, 2].map((i) => (
                  <input key={i} className="input bs-in" type="number" inputMode="numeric" min="0"
                         value={sets[i].a} onChange={(e) => updateSet(i, 'a', e.target.value)} />
                ))}
              </div>
              <div className="bs-row">
                <span className="bs-team">{teamText(b)}</span>
                {[0, 1, 2].map((i) => (
                  <input key={i} className="input bs-in" type="number" inputMode="numeric" min="0"
                         value={sets[i].b} onChange={(e) => updateSet(i, 'b', e.target.value)} />
                ))}
              </div>
            </div>
          </div>
        )}

        <div className="modal-actions board-modal-actions">
          <button type="button" className="btn btn-ghost" onClick={clearAll}>Platz leeren</button>
          <div className="spacer" />
          <button type="button" className="btn btn-ghost" onClick={onClose}>Abbrechen</button>
          <button type="button" className="btn btn-primary" onClick={() => onSave(index, { a: a || null, b: b || null, sets: buildSets() })}>Speichern</button>
        </div>
      </div>
    </div>
  );
}

function LivescorePage({ board, nextList, registrations, bracket, isAdmin, onSaveBoard, onSaveNext }) {
  const [editing, setEditing] = useState(null); // Platz-Index oder null
  const [editingNext, setEditingNext] = useState(false);
  const [fs, setFs] = useState(false);

  // Mobile-Erkennung fuer die alternative Karten-Anordnung im Bild
  // (COURT_POS_M + Pfeil-Overlay). Gleiche Grenze wie der CSS-Stapel.
  const [isMobile, setIsMobile] = useState(() => window.matchMedia('(max-width: 700px)').matches);
  useEffect(() => {
    const mq = window.matchMedia('(max-width: 700px)');
    const onChange = (e) => setIsMobile(e.matches);
    mq.addEventListener('change', onChange);
    return () => mq.removeEventListener('change', onChange);
  }, []);

  const courts = (board && board.length === 6) ? board : EMPTY_BOARD;

  const regById = useMemo(() => {
    const m = {};
    for (const r of registrations) m[r.id] = r;
    return m;
  }, [registrations]);

  const upcoming = useMemo(
    () => upcomingMatches(bracket, courts, nextList),
    [bracket, courts, nextList]
  );

  const toggleFullscreen = () => {
    const el = document.documentElement;
    if (!document.fullscreenElement) {
      (el.requestFullscreen ? el.requestFullscreen() : Promise.resolve()).then(() => setFs(true)).catch(() => {});
    } else {
      (document.exitFullscreen ? document.exitFullscreen() : Promise.resolve()).then(() => setFs(false)).catch(() => {});
    }
  };

  const save = (index, slot) => {
    const next = courts.map((c, i) => (i === index ? slot : c));
    onSaveBoard(next);
    setEditing(null);
  };

  return (
    <div className="board-page">
      {/* Schwarze Kopfleiste im Website-Topbar-Look (rote Kante unten):
          Wappen + Turniername links, Datum/Verein rechts, dazu dezente
          Controls (Website / Vollbild) — ersetzt Masthead + Hover-Chrome. */}
      <div className="board-topbar">
        <div className="board-brand">
          <img className="board-crest" src="assets/wappen3.png?v=2026-w4" alt="TSV Baar-Ebenhausen" />
          <div className="board-title hero-title-display"><span className="ord">1.</span> Paartal Open</div>
        </div>
        <div className="board-meta">
          <div className="board-meta-date">07.–09. August 2026</div>
          <div className="board-meta-club">TSV Baar-Ebenhausen</div>
        </div>
        {/* Vollbild: dezentes Overlay ueber dem Datum, nur bei Hover
            sichtbar (Topbar-Hover) — der TV sieht nur Datum + Verein. */}
        <button type="button" className="board-top-btn" onClick={toggleFullscreen} aria-label={fs ? 'Vollbild aus' : 'Vollbild'}>
          <Icon name={fs ? 'minimize' : 'maximize'} size={14} />
        </button>
      </div>

      <SponsorRail />

      <div className="board-stage">
        {COURT_POS.map((_, i) => (
          <CourtLabel key={i} index={i} slot={courts[i]} regById={regById}
                      isAdmin={isAdmin} onEdit={setEditing} mobile={isMobile} />
        ))}
        {/* Broadcast-Streifen unten im Bild: Website-URL zwischen roten
            Strichen, auf weichem Dunkel-Verlauf. */}
        <div className="board-foot"><span>paartal-open.de</span></div>
      </div>

      <NextRail matches={upcoming} regById={regById} isAdmin={isAdmin}
                onEdit={() => setEditingNext(true)} />

      {editing !== null && (
        <BoardModal
          index={editing}
          slot={courts[editing]}
          registrations={registrations}
          bracket={bracket}
          regById={regById}
          onSave={save}
          onClose={() => setEditing(null)}
        />
      )}

      {editingNext && (
        <NextModal
          nextObj={nextList || { manual: [], hidden: [] }}
          upcoming={upcoming}
          bracket={bracket}
          registrations={registrations}
          regById={regById}
          onSave={onSaveNext}
          onClose={() => setEditingNext(false)}
        />
      )}
    </div>
  );
}

Object.assign(window, { LivescorePage });
