// ===== Obere Ansicht: eingeplante Reihenfolge (Gantt-Zeitleiste + Tabelle) =====

// Belegung berechnen — kapazitätsbewusst: nur in offenen Schichten/Tagen einplanen,
// geschlossene Tage (0 Schichten) werden übersprungen. Rüstzeiten kommen aus der Matrix.
function computeSchedule(ops, opts = {}) {
  const DAY = 24;
  const capH = window.__capDayHours; // (dayIndex) => verfügbare Stunden, sonst durchgehend
  const dayCap = (d) => capH ? capH(d) : DAY;

  // t auf den nächsten offenen Arbeitszeit-Punkt vorrücken
  const advance = (t) => {
    for (let g = 0; g < 4000; g++) {
      const d = Math.floor(t / DAY), cap = dayCap(d), we = d * DAY + cap;
      if (cap <= 0) { t = (d + 1) * DAY; continue; }
      if (t < d * DAY) t = d * DAY;
      if (t >= we - 1e-9) { t = (d + 1) * DAY; continue; }
      return t;
    }
    return t;
  };

  const dayEnd = (t) => { const d = Math.floor(t / DAY); return d * DAY + dayCap(d); };
  const splittable = opts.splittable || window.__splittable || new Set();
  const gapFill = !!opts.gapFill;
  // maximale Tageskapazität (für Vorgänge, die in keinen Tag passen → Overflow statt Endlossuche)
  let maxDayCap = 0; for (let d = 0; d < 14; d++) maxDayCap = Math.max(maxDayCap, dayCap(d));

  // Einen Vorgang ab Zeit t platzieren (kapazitäts- & rüstbewusst, optional gesplittet)
  const place = (o, t, prevMat) => {
    const setupMin = window.setupMinutes(prevMat, o.mat);
    const setupH = setupMin / 60;
    const canSplit = splittable.has(o.nr);
    t = advance(Math.max(t, o.earliestH || 0));
    if (!canSplit) {
      const need = setupH + o.dauerH;
      // passt der Vorgang in keinen Tag (z.B. sehr niedrige OEE)? → an nächstem offenen Tag platzieren (Overflow)
      if (need > maxDayCap + 1e-9) {
        const sStart = t, opStart = t + setupH, end = t + need;
        return { setupMin, setupStart: sStart, opStart, end, segments: [{ start: opStart, end }], split: false, overflow: true };
      }
      for (let g = 0; g < 800; g++) {
        const d = Math.floor(t / DAY);
        if (dayEnd(t) - t >= need - 1e-9) break;
        t = advance((d + 1) * DAY);
      }
      const sStart = t, opStart = t + setupH, end = t + need;
      return { setupMin, setupStart: sStart, opStart, end, segments: [{ start: opStart, end }], split: false };
    }
    for (let g = 0; g < 800; g++) {
      const d = Math.floor(t / DAY);
      if (dayEnd(t) - t >= setupH + 1e-9) break;
      t = advance((d + 1) * DAY);
    }
    const sStart = t; let tt = t + setupH; const opStart = tt;
    let remaining = o.dauerH; const segments = [];
    for (let g = 0; g < 4000 && remaining > 1e-9; g++) {
      tt = advance(tt);
      const avail = dayEnd(tt) - tt;
      const use = Math.min(avail, remaining);
      if (use > 1e-9) { segments.push({ start: tt, end: tt + use }); tt += use; remaining -= use; }
      else { tt = advance((Math.floor(tt / DAY) + 1) * DAY); }
    }
    const end = segments.length ? segments[segments.length - 1].end : opStart;
    return { setupMin, setupStart: sStart, opStart, end, segments, split: segments.length > 1 };
  };

  // frühestmögliche Startzeit (für Auswahl im Lückenfüll-Modus)
  const readyTime = (o) => Math.max(0, o.earliestH || 0);

  let prevMat = opts.startMat ?? null;
  let t = opts.startT ?? 0;
  const remaining = ops.map((o, i) => ({ o, i }));
  const blocks = [];
  let runIdx = 0;
  while (remaining.length) {
    let pick = 0; // Index in remaining
    if (gapFill && !remaining[0].o.__locked) {
      // Fenster = nicht-fixierte Vorgänge bis zur nächsten Fixierung (Barriere)
      let barrier = remaining.findIndex(r => r.o.__locked);
      if (barrier < 0) barrier = remaining.length;
      // unter den verfügbaren (bereits angekommenen) den ersten der Reihenfolge wählen
      let avail = -1, earliest = -1, earliestT = Infinity;
      for (let k = 0; k < barrier; k++) {
        const rt = readyTime(remaining[k].o);
        if (rt <= t + 1e-9) { avail = k; break; }
        if (rt < earliestT) { earliestT = rt; earliest = k; }
      }
      pick = avail >= 0 ? avail : (earliest >= 0 ? earliest : 0);
    }
    const { o } = remaining.splice(pick, 1)[0];
    const b = place(o, t, prevMat);
    blocks.push({ op: o, idx: runIdx++, ...b });
    prevMat = o.mat;
    t = b.end;
  }
  return { blocks, total: blocks.length ? blocks[blocks.length - 1].end : (opts.startT ?? 0) };
}

function Gantt({ blocks, frozenEndH, solving, shiftPx: shiftPx0 = 74, lockedSet, selected, onSelect }) {
  const [shiftPx, setShiftPx] = React.useState(shiftPx0);
  const DAY_H = window.SHIFT_HOURS * 3;
  const maxEnd = Math.max(0, ...blocks.map(b => b.end), frozenEndH);
  // Weiter Zeitbereich: Standard zeigt ~1 Woche, scrollbar weit in Vergangenheit & Zukunft.
  const pastDays = 180;
  const futureDays = Math.max(180, Math.ceil(maxEnd / 24) + 21);
  const startDay = -pastDays, totalDays = pastDays + futureDays;
  const startH = startDay * 24;
  const px = (h) => ((h - startH) / window.SHIFT_HOURS) * shiftPx;
  const pxw = (h) => (h / window.SHIFT_HOURS) * shiftPx; // Breite (ohne Ursprungs-Offset)
  const W = totalDays * 3 * shiftPx;
  const frozenShiftCount = frozenEndH / window.SHIFT_HOURS;
  const openShifts = (d) => window.__openShifts ? window.__openShifts(d) : 3;
  const shiftOpen = (i) => (i % 3) < openShifts(Math.floor(i / 3));
  // initial auf "heute" scrollen
  const scrollRef = React.useRef(null);
  const didScroll = React.useRef(false);
  React.useEffect(() => {
    if (scrollRef.current && !didScroll.current) { scrollRef.current.scrollLeft = px(0) - shiftPx * 3; didScroll.current = true; }
  });
  // Zoom per Mausrad (Cursor-verankert). Standard ~1 Woche; reinzoomen zeigt Uhrzeiten.
  const onWheel = (e) => {
    if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) return; // horizontal scrollen erlauben
    e.preventDefault();
    const el = scrollRef.current; if (!el) return;
    const rect = el.getBoundingClientRect();
    const cursorContentX = e.clientX - rect.left + el.scrollLeft;
    const factor = e.deltaY < 0 ? 1.18 : 1 / 1.18;
    setShiftPx(prev => {
      const next = Math.max(28, Math.min(900, prev * factor));
      const ratio = next / prev;
      requestAnimationFrame(() => { if (el) el.scrollLeft = cursorContentX * ratio - (e.clientX - rect.left); });
      return next;
    });
  };
  const hourPx = shiftPx / window.SHIFT_HOURS;
  const showTimes = shiftPx >= 110;     // Schicht-Startzeiten
  const showHours = hourPx >= 26;       // stündliche Ticks
  const shiftStart = (i) => ['06:00', '14:00', '22:00'][((i % 3) + 3) % 3];
  // Gitter via CSS-Gradient (Schichtlinien + kräftigere Tageslinien) — performant über riesige Breite
  const gridBg = `repeating-linear-gradient(90deg, var(--line) 0 1px, transparent 1px ${shiftPx}px), repeating-linear-gradient(90deg, var(--line-2) 0 1px, transparent 1px ${shiftPx * 3}px)`;
  // sichtbares Fenster für Detail-Elemente (Schichtlabels, geschlossene Schichten) begrenzen
  const winFrom = -14, winTo = Math.ceil(maxEnd / 24) + 21;

  const renderBlock = (b, kind) => {
    const frozen = kind === 'frozen', pinned = kind === 'pinned';
    const m = window.MATERIALS[b.op.mat];
    const sc = window.__score(b.op);
    const sel = selected === b.op.nr;
    const segs = b.segments && b.segments.length ? b.segments : [{ start: b.opStart, end: b.end }];
    return segs.map((seg, si) => {
      const left = px(seg.start), width = Math.max(pxw(seg.end - seg.start), 16);
      const first = si === 0, last = si === segs.length - 1, isSplit = segs.length > 1;
      return (
        <div key={b.op.nr + '-' + si} onClick={() => onSelect && onSelect(sel ? null : b.op.nr)}
          title={`${b.op.nr} · ${b.op.kurz}${pinned ? ' · fixiert' : ''}${isSplit ? ` · Teil ${si + 1}/${segs.length} (gesplittet)` : ''}`}
          style={{ position: 'absolute', left, width, top: 0, bottom: 0, cursor: 'pointer',
            background: sel ? 'var(--accent)' : frozen ? 'var(--frozen)' : `color-mix(in oklch, ${m.color} 26%, var(--surface))`,
            border: `1px solid ${sel ? 'var(--accent-ink)' : pinned ? 'var(--accent-ink)' : frozen ? 'var(--line-2)' : `color-mix(in oklch, ${m.color} 50%, var(--line))`}`,
            borderLeft: first ? `3px solid ${sel || pinned ? 'var(--accent-ink)' : m.color}` : `1px dashed ${pinned ? 'var(--accent-ink)' : `color-mix(in oklch, ${m.color} 60%, var(--line))`}`,
            borderRight: !last ? `1px dashed color-mix(in oklch, ${m.color} 60%, var(--line))` : undefined,
            borderTopLeftRadius: first ? 4 : 0, borderBottomLeftRadius: first ? 4 : 0,
            borderTopRightRadius: last ? 4 : 0, borderBottomRightRadius: last ? 4 : 0,
            boxShadow: sel ? '0 0 0 2px var(--accent-ink)' : pinned ? 'inset 0 0 0 1px var(--accent-ink)' : 'none',
            zIndex: sel ? 2 : 1,
            padding: '4px 6px', overflow: 'hidden',
            display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
            animation: !frozen && solving ? 'rowIn .4s both' : 'none',
            animationDelay: !frozen && solving ? (b.idx * 60) + 'ms' : '0s',
          }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 4, justifyContent: 'space-between' }}>
            <span className="num" style={{ fontFamily: 'var(--mono)', fontSize: 10.5, fontWeight: 600, color: 'var(--ink)' }}>
              {b.op.nr.replace('FA-', '')}{isSplit && <span style={{ color: 'var(--muted)', fontWeight: 400 }}>·{si + 1}</span>}
            </span>
            {first && (frozen ? <Icon name="lock" size={11} style={{ color: 'var(--muted)' }} />
              : pinned ? <Icon name="lock" size={11} style={{ color: 'var(--accent-ink)' }} />
              : <ScoreBadge value={sc} size="sm" />)}
          </div>
          {first && <div style={{ fontSize: 10, color: 'var(--ink-2)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{b.op.kurz}</div>}
        </div>
      );
    });
  };

  const renderSetup = (b) => {
    if (b.setupMin < 12) return null;
    const left = px(b.setupStart), width = pxw(b.opStart - b.setupStart);
    return <div key={'s' + b.op.nr} title={`Rüsten ${b.setupMin} min`} style={{
      position: 'absolute', left, width: Math.max(width, 2), top: 0, bottom: 0,
      background: 'repeating-linear-gradient(45deg, var(--line-2) 0 3px, transparent 3px 6px)',
      opacity: .8, borderRadius: 2 }} />;
  };

  return (
    <div ref={scrollRef} onWheel={onWheel} style={{ overflowX: 'auto', overflowY: 'hidden' }}>
      <div style={{ width: W, position: 'relative' }}>
        {/* Tages-Kopf mit Datum */}
        <div style={{ position: 'relative', height: 22 }}>
          {Array.from({ length: totalDays }, (_, k) => {
            const d = startDay + k;
            const closed = openShifts(d) === 0;
            return (
              <div key={k} style={{ position: 'absolute', left: px(d * 24), width: shiftPx * 3, height: '100%', borderLeft: '1px solid var(--line-2)', paddingLeft: 8,
                display: 'flex', alignItems: 'center', gap: 6, boxSizing: 'border-box',
                background: (d * 24 < frozenEndH && d >= 0) ? 'color-mix(in oklch, var(--frozen) 32%, transparent)' : 'transparent' }}>
                <span className="num" style={{ fontFamily: 'var(--mono)', fontSize: 11.5, fontWeight: 600, letterSpacing: '.03em',
                  color: closed ? 'var(--muted)' : d < 0 ? 'var(--muted)' : 'var(--ink-2)', textDecoration: closed ? 'line-through' : 'none' }}>{window.fmtDate(d * 24, true)}</span>
                {closed && <span style={{ fontSize: 9, color: 'var(--muted)', fontFamily: 'var(--mono)', textTransform: 'uppercase', letterSpacing: '.06em' }}>geschl.</span>}
              </div>
            );
          })}
        </div>
        {/* Schicht-Kopf (nur im Fenster um „heute") */}
        <div style={{ position: 'relative', height: 17, marginBottom: 4 }}>
          {Array.from({ length: (winTo - winFrom) * 3 }, (_, j) => {
            const i = winFrom * 3 + j;
            const open = shiftOpen(i);
            return (
              <div key={i} style={{ position: 'absolute', left: px(i * window.SHIFT_HOURS), width: shiftPx, height: '100%',
                paddingLeft: 6, display: 'flex', alignItems: 'center', gap: 5, boxSizing: 'border-box', overflow: 'hidden' }}>
                <span className="num" style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '.06em',
                  color: open ? 'var(--muted)' : 'var(--line-2)', textDecoration: open ? 'none' : 'line-through' }}>{window.SHIFT_NAMES[((i % 3) + 3) % 3]}</span>
                {showTimes && <span className="num" style={{ fontFamily: 'var(--mono)', fontSize: 9, color: 'var(--line-2)' }}>{shiftStart(i)}</span>}
              </div>
            );
          })}
          {showHours && Array.from({ length: (winTo - winFrom) * 24 }, (_, j) => {
            const hh = winFrom * 24 + j; const hourOfDay = ((hh % 24) + 24) % 24;
            if (hourOfDay % 2 !== 0) return null;
            return <span key={'h' + hh} className="num" style={{ position: 'absolute', left: px(hh) + 1, bottom: -1, fontFamily: 'var(--mono)', fontSize: 8, color: 'var(--muted)' }}>{String(hourOfDay).padStart(2, '0')}</span>;
          })}
        </div>

        {/* Track */}
        <div style={{ position: 'relative', height: 46, background: `var(--surface-2)`, backgroundImage: showHours ? `repeating-linear-gradient(90deg, var(--line) 0 1px, transparent 1px ${hourPx}px), ${gridBg}` : gridBg, borderRadius: 5, border: '1px solid var(--line)' }}>
          {/* geschlossene Schichten (nur im Fenster) */}
          {Array.from({ length: (winTo - winFrom) * 3 }, (_, j) => { const i = winFrom * 3 + j; return !shiftOpen(i) ? i : null; }).filter(i => i !== null).map(i => (
            <div key={'cl' + i} title="geschlossen — keine Kapazität" style={{ position: 'absolute', left: px(i * window.SHIFT_HOURS), width: shiftPx, top: 0, bottom: 0,
              background: 'repeating-linear-gradient(45deg, color-mix(in oklch, var(--bad) 30%, var(--surface-2)) 0 4px, var(--surface-2) 4px 9px)',
              pointerEvents: 'none' }} />
          ))}
          {/* Frozen-Zone-Overlay */}
          <div style={{ position: 'absolute', left: px(0), width: px(frozenEndH) - px(0), top: 0, bottom: 0,
            background: 'repeating-linear-gradient(135deg, color-mix(in oklch, var(--frozen) 60%, transparent) 0 8px, transparent 8px 16px)',
            borderRight: '1.5px dashed var(--line-2)', pointerEvents: 'none' }} />
          <div style={{ position: 'absolute', left: px(frozenEndH) - 4, top: 3, transform: 'translateX(-100%)' }}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 9, color: 'var(--muted)', fontFamily: 'var(--mono)', letterSpacing: '.08em', textTransform: 'uppercase' }}>
              <Icon name="lock" size={9} /> Frozen Zone
            </span>
          </div>
          {/* „Jetzt"-Linie */}
          <div style={{ position: 'absolute', left: px(0), top: 0, bottom: 0, borderLeft: '1.5px solid var(--accent-ink)', pointerEvents: 'none' }} />
          {/* Blöcke */}
          <div style={{ position: 'absolute', inset: '3px 0' }}>
            {blocks.map(b => {
              const locked = lockedSet && lockedSet.has(b.op.nr);
              const kind = locked ? (b.op.released ? 'frozen' : 'pinned') : 'solved';
              return [renderSetup(b), renderBlock(b, kind)];
            })}
          </div>
        </div>
      </div>
    </div>
  );
}

function SequenceTable({ blocks, solving, lockedSet, onToggleLock, onReorder, onRemove, splitSet, onToggleSplit, selected, onSelect, onLosClick }) {
  const [sort, toggleSort] = window.useSort('pos', 1);
  const hToShift = (h) => {
    // Tag und Schicht direkt aus der Stunde ableiten (unabhängig von der SHIFTS-Array-Länge),
    // damit Tabelle, Gantt und Durchlaufplanung immer denselben Tag zeigen.
    const shiftInDay = Math.floor((h % 24) / window.SHIFT_HOURS); // 0..2
    const name = window.SHIFT_NAMES[Math.max(0, Math.min(2, shiftInDay))];
    return `${window.fmtDate(h, true)} ${name}`;
  };
  const Row = ({ b, pos }) => {
    const locked = lockedSet && lockedSet.has(b.op.nr);
    const released = b.op.released;
    const canSplit = splitSet && splitSet.has(b.op.nr);
    const sel = selected === b.op.nr;
    const sc = window.__score(b.op);
    return (
      <tr draggable
        onClick={() => onSelect && onSelect(sel ? null : b.op.nr)}
        onDragStart={e => { e.dataTransfer.setData('text/plain', b.op.nr); e.dataTransfer.effectAllowed = 'move'; }}
        onDragOver={e => { if (e.dataTransfer.types.includes('text/plain')) e.preventDefault(); }}
        onDrop={e => { e.preventDefault(); e.stopPropagation(); const nr = e.dataTransfer.getData('text/plain'); if (nr && nr !== b.op.nr) onReorder(nr, b.op.nr); }}
        title="Klicken zum Hervorheben · Ziehen zum Umsortieren"
        style={{ borderBottom: '1px solid var(--line)', cursor: 'grab',
          background: sel ? 'var(--accent)' : locked ? (released ? 'color-mix(in oklch, var(--frozen) 34%, transparent)' : 'color-mix(in oklch, var(--accent) 20%, transparent)') : 'transparent',
          boxShadow: sel ? 'inset 3px 0 0 var(--accent-ink)' : 'none',
          animation: solving ? 'rowIn .4s both' : 'none', animationDelay: solving ? (b.idx * 45) + 'ms' : '0s' }}>
        <td style={tdNum}>{pos}</td>
        <td style={td}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <button onClick={e => { e.stopPropagation(); onToggleLock(b.op.nr); }} title={locked ? 'Fixierung lösen' : 'Fixieren'}
              style={{ width: 19, height: 19, borderRadius: 5, display: 'grid', placeItems: 'center', cursor: 'pointer', flex: '0 0 auto',
                border: '1px solid ' + (locked ? 'var(--accent-ink)' : 'var(--line-2)'),
                background: locked ? 'var(--accent-ink)' : 'var(--surface)', color: locked ? '#fff' : 'var(--muted)' }}>
              <Icon name="lock" size={11} />
            </button>
            <span className="num" style={{ fontFamily: 'var(--mono)', fontWeight: 600 }}>{b.op.nr}</span>
            <span style={{ color: 'var(--muted)', fontFamily: 'var(--mono)', fontSize: 11 }}>/{b.op.avo}</span>
            {released && <span className="label" style={{ fontSize: 9 }}>freigegeben</span>}
          </div>
        </td>
        <td style={td}>{b.op.kurz}</td>
        <td style={td}><MatChip mat={b.op.mat} /></td>
        <td style={{ ...tdNum, textAlign: 'right' }}>
          <button onClick={e => { e.stopPropagation(); onLosClick && onLosClick({ ...b.op, _setupMin: b.setupMin }); }} title="Losgröße berechnen (Andler)"
            style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '2px 7px', borderRadius: 5, cursor: 'pointer',
              border: '1px solid var(--line-2)', background: 'var(--surface)', color: 'var(--ink)', fontFamily: 'var(--mono)', fontSize: 11.5, fontWeight: 600 }}>
            {b.op.menge}<Icon name="calc" size={11} style={{ color: 'var(--muted)' }} />
          </button>
        </td>
        <td style={tdNum}>
          {b.setupMin >= 12
            ? <span style={{ color: b.setupMin >= 40 ? 'var(--bad-ink)' : 'var(--ink-2)' }}>{b.setupMin}′</span>
            : <span style={{ color: 'var(--good-ink)' }}>{b.setupMin}′</span>}
        </td>
        <td style={tdNum}>{(b.op.einzelzeitH != null ? b.op.einzelzeitH * 60 : (b.op.dauerH * 60 / Math.max(1, b.op.menge))).toFixed(1).replace('.', ',')}′</td>
        <td style={tdNum}>{(b.setupMin / 60 + b.op.dauerH).toFixed(1).replace('.', ',')} h</td>
        <td style={td}><ScoreBadge value={sc} size="sm" dim={locked} /></td>
        <td style={{ ...td, textAlign: 'center' }}>
          <label title={canSplit ? 'Splitten über Schichten/Tage erlaubt' : 'Nicht splitten — am Stück einplanen'} onClick={e => e.stopPropagation()}
            style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
            <input type="checkbox" checked={!!canSplit} onChange={() => onToggleSplit(b.op.nr)}
              style={{ width: 15, height: 15, accentColor: 'var(--accent-ink)', cursor: 'pointer', margin: 0 }} />
          </label>
          {b.split && <span title="wird aktuell gesplittet" style={{ marginLeft: 5, fontFamily: 'var(--mono)', fontSize: 9.5, color: 'var(--accent-ink)', verticalAlign: 'middle' }}>{b.segments.length}×</span>}
        </td>
        <td style={tdNum}>{hToShift(b.opStart)}</td>
        <td style={tdNum}>{hToShift(b.end - 0.01)}</td>
      </tr>
    );
  };
  const acc = (b, key) => {
    switch (key) {
      case 'pos': return b.idx;
      case 'nr': return b.op.nr;
      case 'kurz': return b.op.kurz;
      case 'mat': return window.MATERIALS[b.op.mat].group + window.MATERIALS[b.op.mat].id;
      case 'setup': return b.setupMin;
      case 'menge': return b.op.menge;
      case 'einzel': return b.op.einzelzeitH != null ? b.op.einzelzeitH : b.op.dauerH / Math.max(1, b.op.menge);
      case 'dauer': return b.setupMin / 60 + b.op.dauerH;
      case 'score': return window.__score(b.op);
      case 'split': return (b.segments ? b.segments.length : 1);
      case 'start': return b.opStart;
      case 'ende': return b.end;
      default: return '';
    }
  };
  const rows = window.sortRows(blocks, sort, acc);
  return (
    <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12.5 }}>
      <thead>
        <tr>
          <SortTh label="#" sortKey="pos" sort={sort} onSort={toggleSort} align="right" />
          <SortTh label="Auftrag / AVO" sortKey="nr" sort={sort} onSort={toggleSort} />
          <SortTh label="Kurztext" sortKey="kurz" sort={sort} onSort={toggleSort} />
          <SortTh label="Werkstoff" sortKey="mat" sort={sort} onSort={toggleSort} />
          <SortTh label="Losgröße" sortKey="menge" sort={sort} onSort={toggleSort} align="right" />
          <SortTh label="Rüst" sortKey="setup" sort={sort} onSort={toggleSort} align="right" />
          <SortTh label="Einzelzeit" sortKey="einzel" sort={sort} onSort={toggleSort} align="right" />
          <SortTh label="Auftragsdauer" sortKey="dauer" sort={sort} onSort={toggleSort} align="right" />
          <SortTh label="Score" sortKey="score" sort={sort} onSort={toggleSort} />
          <SortTh label="Split" sortKey="split" sort={sort} onSort={toggleSort} align="center" />
          <SortTh label="Start" sortKey="start" sort={sort} onSort={toggleSort} align="right" />
          <SortTh label="Ende" sortKey="ende" sort={sort} onSort={toggleSort} align="right" />
        </tr>
      </thead>
      <tbody>
        {rows.map(b => <Row key={b.op.nr} b={b} pos={b.idx + 1} />)}
      </tbody>
    </table>
  );
}

const thStyle = { fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '.1em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 500, padding: '6px 10px', textAlign: 'left', borderBottom: '1px solid var(--line-2)', position: 'sticky', top: 0, background: 'var(--panel)', zIndex: 1 };
const td = { padding: '7px 10px', color: 'var(--ink)', verticalAlign: 'middle' };
const tdNum = { ...td, textAlign: 'right', fontVariantNumeric: 'tabular-nums', fontFamily: 'var(--mono)', fontSize: 11.5, color: 'var(--ink-2)' };

Object.assign(window, { computeSchedule, Gantt, SequenceTable, thStyle, td, tdNum });
