/* Dispatch — Developer lane (Now / Next / Later dev board + AI priority)
 * Loaded by index.html after lib.jsx and hub.jsx (uses TicketDetailModal +
 * LaneBadge) — before app.jsx.
 *
 * Faithful Tailwind port of the old DevBoardView + AiPriorityCard: a curated
 * three-tier board (Now has a WIP limit, Later is the paginated long tail)
 * that triagers reorder via drag-and-drop or quick-move buttons
 * (moveDevBoardTicket), with an AI "What's hot right now" top-N card above it
 * (fetchAiPriorityTop / refreshAiPriority). Live via fetchDevBoard. */

const DEV_TIERS = [
  { id: 'now', label: 'Now', sub: 'Actively in flight', color: '#cf222e' },
  { id: 'next', label: 'Next', sub: 'Ready when capacity frees', color: '#bf8700' },
  { id: 'later', label: 'Later', sub: 'Long tail backlog', color: '#6b7280' },
];
const DEV_LATER_PAGE = 10; // Later loads/pages 10 at a time (server-side)

// Map a board card / AI item into the shape TicketDetailModal reads.
function devToItem(c) {
  return {
    id: c.id ?? c.ticketId,
    lane: 'dev',
    num: c.num,
    title: c.title,
    priority: c.priority,
    status: c.status,
    assigneeName: c.assigneeName,
    assigneeLogin: c.assigneeLogin,
    requester: c.assigneeName || c.assigneeLogin || '—',
    age: relAge(c.sourceUpdatedAt || c.sourceCreatedAt),
    sourceUrl: c.sourceUrl,
  };
}

function DevBoardCard({ card, isTriager, busy, onOpen, onDragStart, onDragOver, onMoveTo, dragging }) {
  const ageDays = card.sourceCreatedAt ? Math.floor((Date.now() - new Date(card.sourceCreatedAt).getTime()) / 86400000) : null;
  const quick = (e, tier) => { e.stopPropagation(); onMoveTo(tier); };
  return (
    <div draggable={isTriager} onDragStart={onDragStart} onDragOver={onDragOver} onClick={onOpen}
      className={cn('rounded-lg border bg-card p-3 shadow-sm transition hover:border-primary/40',
        isTriager ? 'cursor-grab active:cursor-grabbing' : 'cursor-pointer',
        dragging && 'opacity-40', card.prodIncident && 'border-destructive/50')}>
      <div className="flex items-center gap-2">
        <span className="font-mono text-[11px] text-muted-foreground">{card.num}</span>
        <Badge variant={prioVariant(card.priority)} className="capitalize">{card.priority || '—'}</Badge>
        {card.prodIncident && <Badge variant="destructive">PROD</Badge>}
      </div>
      <div className="mt-1.5 line-clamp-2 text-sm font-medium leading-snug">{card.title}</div>
      <div className="mt-2 flex items-center gap-1.5 text-[11px] text-muted-foreground">
        <span className="truncate">{card.repoName}</span>
        <span className="truncate">· {card.assigneeName || card.assigneeLogin || 'unassigned'}</span>
        {ageDays != null && <span className="ml-auto shrink-0">{ageDays}d</span>}
      </div>
      {isTriager && (
        <div className="mt-2.5 flex flex-wrap gap-1.5">
          {card.tier !== 'now' && <Button size="sm" variant="outline" disabled={busy} onClick={e => quick(e, 'now')}>→ Now</Button>}
          {card.tier !== 'next' && <Button size="sm" variant="outline" disabled={busy} onClick={e => quick(e, 'next')}>→ Next</Button>}
          {card.tier !== 'later' && <Button size="sm" variant="ghost" disabled={busy} onClick={e => quick(e, 'later')}>→ Later</Button>}
        </div>
      )}
    </div>
  );
}

function DevBoardRepoFilter({ repos, active, onChange }) {
  const [showAll, setShowAll] = useState(false);
  const total = repos.reduce((a, r) => a + (r.count || 0), 0);
  const VISIBLE = 6;
  const visible = showAll ? repos : repos.slice(0, VISIBLE);
  const overflow = showAll ? [] : repos.slice(VISIBLE);
  const chip = (on) => cn('rounded-full border px-3 py-1 text-xs font-medium transition-colors', on ? 'border-primary bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted');
  return (
    <div className="flex flex-wrap items-center gap-1.5">
      <button className={chip(active == null)} onClick={() => onChange(null)}>All repos <span className="ml-1 opacity-70">{total}</span></button>
      {visible.map(r => (
        <button key={r.repoId} className={chip(active === r.repoId)} onClick={() => onChange(r.repoId)} title={r.repoFullName}>
          {r.repoName} <span className="ml-1 opacity-70">{r.count}</span>
        </button>
      ))}
      {overflow.length > 0 && <button className={chip(false)} onClick={() => setShowAll(true)}>+{overflow.length} more</button>}
      {showAll && repos.length > VISIBLE && <button className={chip(false)} onClick={() => setShowAll(false)}>show fewer</button>}
    </div>
  );
}

function AiPriorityCard({ data, busy, error, isTriager, open, onToggle, onRefresh, onPromote, onOpen }) {
  const items = data?.items ?? [];
  const ranking = data?.ranking;
  const empty = !data || !ranking;
  return (
    <Card>
      <div className="flex cursor-pointer items-center justify-between gap-2 p-4" onClick={onToggle}>
        <div className="flex flex-wrap items-baseline gap-2">
          <span className="text-primary">✦</span>
          <h3 className="font-semibold tracking-tight">What's hot right now</h3>
          <span className="text-xs text-muted-foreground">AI-curated · {items.length > 0 ? `top ${items.length} of ${ranking.candidateCount}` : 'not yet computed'}</span>
        </div>
        <div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
          {ranking?.rankedAt && <span className="hidden text-xs text-muted-foreground sm:inline">Computed {api.relativeTime(ranking.rankedAt)} ago</span>}
          {isTriager && <Button size="sm" variant="outline" onClick={onRefresh} disabled={busy}>{busy ? 'Refreshing…' : (empty ? 'Compute now' : 'Refresh')}</Button>}
          <Button size="sm" variant="ghost" onClick={onToggle}>{open ? '▾' : '▸'}</Button>
        </div>
      </div>
      {open && (
        <div className="border-t p-4">
          {error && <div className="mb-3 rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">{error}</div>}
          {empty && !error && <div className="py-1 text-sm italic text-muted-foreground">{isTriager ? 'No ranking yet. Click "Compute now" to ask Claude to rank the dev backlog.' : 'No ranking yet. A triager can compute one.'}</div>}
          {!empty && items.length === 0 && !error && <div className="py-1 text-sm italic text-muted-foreground">{ranking.summary || 'Nothing to rank — backlog is clear.'}</div>}
          {!empty && items.length > 0 && (
            <>
              {ranking.summary && <div className="mb-3 rounded-md bg-muted/50 px-3 py-2 text-sm text-muted-foreground">{ranking.summary}</div>}
              <ol className="space-y-2">
                {items.map(item => (
                  <li key={item.ticketId} className={cn('flex gap-3 rounded-lg border p-3', item.prodIncident && 'border-destructive/40')}>
                    <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">{item.rank}</div>
                    <div className="min-w-0 flex-1">
                      <div className="flex flex-wrap items-center gap-2">
                        <button className="text-left text-sm font-medium hover:underline" onClick={() => onOpen(item)}><span className="mr-1.5 font-mono text-xs text-muted-foreground">{item.num}</span>{item.title}</button>
                        {item.prodIncident && <Badge variant="destructive">PROD</Badge>}
                        {item.currentTier === 'now' && <Badge>in Now</Badge>}
                        {item.currentTier === 'next' && <Badge variant="secondary">in Next</Badge>}
                      </div>
                      {item.reasoning && <div className="mt-1 text-sm text-muted-foreground">{item.reasoning}</div>}
                      <div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[10px] text-muted-foreground">
                        {(item.signals || []).slice(0, 4).map(s => <Badge key={s} variant="outline">{s.replace(/_/g, ' ')}</Badge>)}
                        {item.repoName && <span>· {item.repoName}</span>}
                        {(item.assigneeName || item.assigneeLogin) && <span>· {item.assigneeName || item.assigneeLogin}</span>}
                      </div>
                    </div>
                    {isTriager && item.currentTier !== 'now' && <Button size="sm" className="self-center" onClick={() => onPromote(item)}>→ Now</Button>}
                  </li>
                ))}
              </ol>
            </>
          )}
        </div>
      )}
    </Card>
  );
}

// Backlog mode — the old DevBacklogView: a flat, filterable/sortable list of
// every dev-lane ticket (from the live feed), with a stat strip and a prod
// incident banner. Read-only; rows open the shared ticket detail.
function DevBacklog({ onOpen }) {
  const [live, setLive] = useState(() => (window.__DISPATCH_LIVE__ && window.__DISPATCH_LIVE__.tickets) || null);
  useEffect(() => {
    const read = () => setLive((window.__DISPATCH_LIVE__ && window.__DISPATCH_LIVE__.tickets) || null);
    read();
    window.addEventListener('dispatch:data-loaded', read);
    return () => window.removeEventListener('dispatch:data-loaded', read);
  }, []);
  const all = (Array.isArray(live) ? live : []).filter(t => t.lane === 'dev');
  const [repo, setRepo] = useState('all');
  const [search, setSearch] = useState('');
  const [priority, setPriority] = useState('all');
  const [page, setPage] = useState(1);
  const [sort, toggleSort] = useSort(null);

  const repos = [...new Set(all.map(t => t.repoName).filter(Boolean))];
  const q = search.trim().toLowerCase();
  const filtered = all.filter(t =>
    (repo === 'all' || t.repoName === repo) &&
    (priority === 'all' || t.priority === priority) &&
    (!q || (t.title || '').toLowerCase().includes(q) || (t.num || '').toLowerCase().includes(q)));
  const sortVal = (t, k) => k === 'priority' ? (PRIORITY_RANK[t.priority] ?? 9) : (t[k] ?? '');
  const rows = sortRows(filtered, sort, sortVal);
  useEffect(() => { setPage(1); }, [repo, search, priority, sort]);
  const pageCount = Math.max(1, Math.ceil(rows.length / PER_PAGE));
  const paged = rows.slice((page - 1) * PER_PAGE, page * PER_PAGE);

  const prod = all.filter(t => t.prodIncident);
  const stats = [
    { label: 'Active backlog', value: all.length },
    { label: 'High / critical', value: all.filter(t => t.priority === 'high' || t.priority === 'critical').length },
    { label: 'In progress', value: all.filter(t => t.status === 'in_progress').length },
  ];

  return (
    <div className="space-y-4">
      <div className="grid gap-4 sm:grid-cols-3">
        {stats.map(s => <Card key={s.label}><CardContent className="pt-5"><div className="text-sm text-muted-foreground">{s.label}</div><div className="mt-1 text-3xl font-semibold tracking-tight">{s.value}</div></CardContent></Card>)}
      </div>
      {prod.length > 0 && (
        <div className="rounded-md border border-destructive/40 bg-destructive/10 px-4 py-2 text-sm text-destructive">
          <strong>PROD · {(prod[0].priority || 'high').toUpperCase()}</strong> — {prod.length} production incident{prod.length === 1 ? '' : 's'} in the dev backlog{prod[0].title ? `: ${prod[0].title}` : ''}.
        </div>
      )}
      <Card>
        <div className="flex flex-wrap items-center gap-2 border-b p-3">
          <Input className="w-56" placeholder="Search the backlog…" value={search} onChange={e => setSearch(e.target.value)} />
          {repos.length > 0 && (
            <Select className="w-44" value={repo} onChange={e => setRepo(e.target.value)}>
              <option value="all">All repos ({all.length})</option>
              {repos.map(r => <option key={r} value={r}>{r}</option>)}
            </Select>
          )}
          <Select className="w-40" value={priority} onChange={e => setPriority(e.target.value)}>
            <option value="all">All priorities</option>
            {['low', 'medium', 'high', 'critical'].map(p => <option key={p} value={p}>{titleCase(p)}</option>)}
          </Select>
          <span className="ml-auto text-xs text-muted-foreground">{rows.length} of {all.length}</span>
        </div>
        <div className="overflow-x-auto">
          <table className="w-full text-sm">
            <thead className="border-b text-muted-foreground">
              <tr className="[&>th]:px-4 [&>th]:py-2.5 [&>th]:text-left [&>th]:font-medium">
                <SortHeader label="Item" sortKey="title" sort={sort} onSort={toggleSort} />
                <SortHeader label="Status" sortKey="status" sort={sort} onSort={toggleSort} />
                <SortHeader label="Priority" sortKey="priority" sort={sort} onSort={toggleSort} />
                <SortHeader label="Repo" sortKey="repoName" sort={sort} onSort={toggleSort} />
                <SortHeader label="Assignee" sortKey="assigneeName" sort={sort} onSort={toggleSort} />
                <SortHeader label="Age" sortKey="age" sort={sort} onSort={toggleSort} align="right" />
              </tr>
            </thead>
            <tbody className="divide-y">
              {all.length === 0 && <tr><td colSpan="6" className="px-4 py-6 text-center text-muted-foreground">{Array.isArray(live) ? 'No open dev tickets.' : 'Sign in on the demo to load live tickets.'}</td></tr>}
              {all.length > 0 && rows.length === 0 && <tr><td colSpan="6" className="px-4 py-6 text-center text-muted-foreground">No items match.</td></tr>}
              {paged.map(t => (
                <tr key={t.id} onClick={() => onOpen(t)} className="cursor-pointer transition-colors hover:bg-muted/50">
                  <td className="max-w-[320px] px-4 py-2.5">
                    <div className="flex items-center gap-2"><span className="truncate font-medium">{t.title}</span>{t.prodIncident && <Badge variant="destructive">PROD</Badge>}</div>
                    <div className="font-mono text-xs text-muted-foreground">{t.num}</div>
                  </td>
                  <td className="px-4 py-2.5"><Badge variant="outline">{titleCase(t.status)}</Badge></td>
                  <td className="px-4 py-2.5"><Badge variant={prioVariant(t.priority)}>{t.priority}</Badge></td>
                  <td className="max-w-[140px] truncate px-4 py-2.5 text-muted-foreground">{t.repoName || '—'}</td>
                  <td className="max-w-[140px] truncate px-4 py-2.5 text-muted-foreground">{t.assigneeName || t.assigneeLogin || '—'}</td>
                  <td className="px-4 py-2.5 text-right text-muted-foreground">{t.age}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        <div className="flex items-center justify-between border-t px-4 py-2">
          <span className="text-xs text-muted-foreground">{rows.length} shown</span>
          <Pagination page={page} pageCount={pageCount} onPage={setPage} />
        </div>
      </Card>
    </div>
  );
}

function DevBoard() {
  const [board, setBoard] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [busy, setBusy] = useState(false);
  const [laterOffset, setLaterOffset] = useState(0);
  const [repoFilterId, setRepoFilterId] = useState(null);
  const [dragId, setDragId] = useState(null);
  const [dropTarget, setDropTarget] = useState(null);
  const [openItem, setOpenItem] = useState(null);
  const [overviewOpen, setOverviewOpen] = useOverviewOpen();
  const [mode, setMode] = useState('board'); // 'board' | 'backlog'
  const [boardTab, setBoardTab] = useState('hot'); // 'hot' | 'kanban'
  const [colPages, setColPages] = useState({}); // client 10/page for Now & Next
  const [isTriager, setIsTriager] = useState(false);
  const [aiTop, setAiTop] = useState(null);
  const [aiBusy, setAiBusy] = useState(false);
  const [aiError, setAiError] = useState(null);
  const [aiOpen, setAiOpen] = useState(true);

  useEffect(() => { api.fetchHelpdeskMe().then(me => setIsTriager(!!me?.isTriager)).catch(() => {}); }, []);

  async function load() {
    setLoading(true);
    try {
      const r = await api.fetchDevBoard({ laterLimit: DEV_LATER_PAGE, laterOffset, repoId: repoFilterId });
      setBoard(r); setError(null);
    } catch (e) { setError(e.message || 'Failed to load board'); }
    finally { setLoading(false); }
  }
  useEffect(() => { load(); }, [laterOffset, repoFilterId]);
  useEffect(() => { setLaterOffset(0); }, [repoFilterId]);
  useEffect(() => {
    const on = () => load();
    window.addEventListener('dispatch:data-loaded', on);
    return () => window.removeEventListener('dispatch:data-loaded', on);
  }, [laterOffset, repoFilterId]);

  async function loadAiTop() {
    try { const r = await api.fetchAiPriorityTop(); setAiTop(r); setAiError(null); }
    catch (e) { setAiError(e.message || 'Could not load AI priorities'); }
  }
  useEffect(() => { loadAiTop(); }, []);
  async function refreshAiTop() {
    if (!isTriager) { setAiError('Only triagers can trigger a fresh AI ranking.'); return; }
    setAiBusy(true); setAiError(null);
    try { const r = await api.refreshAiPriority(); setAiTop(r); }
    catch (e) { setAiError(e.message || 'Refresh failed'); }
    finally { setAiBusy(false); }
  }

  async function moveCard(card, targetTier, targetPosition) {
    if (!isTriager) { setError('Only triagers can move cards on the board.'); return; }
    setBusy(true);
    const prev = board;
    setBoard(opt => {
      if (!opt) return opt;
      const without = {
        now: (opt.now || []).filter(c => c.id !== card.id),
        next: (opt.next || []).filter(c => c.id !== card.id),
        later: (opt.later || []).filter(c => c.id !== card.id),
      };
      const list = without[targetTier];
      const idx = targetPosition == null ? list.length : Math.min(targetPosition, list.length);
      const updated = [...list.slice(0, idx), { ...card, tier: targetTier }, ...list.slice(idx)];
      return { ...opt, ...without, [targetTier]: updated };
    });
    try { await api.moveDevBoardTicket(card.id, targetTier, targetPosition); await load(); }
    catch (e) { setError(e.message || 'Move failed'); setBoard(prev); }
    finally { setBusy(false); }
  }

  function onDragStart(card) { setDragId(card.id); }
  function onDragOverCol(e, tier) { if (!dragId) return; e.preventDefault(); setDropTarget({ tier, position: null }); }
  function onDragOverCard(e, tier, position) { if (!dragId) return; e.preventDefault(); e.stopPropagation(); setDropTarget({ tier, position }); }
  function onDrop(e, tier) {
    e.preventDefault();
    if (!dragId || !board) { setDragId(null); return; }
    const card = (board.now || []).find(c => c.id === dragId) || (board.next || []).find(c => c.id === dragId) || (board.later || []).find(c => c.id === dragId);
    const pos = dropTarget && dropTarget.tier === tier ? dropTarget.position : null;
    setDragId(null); setDropTarget(null);
    if (card) void moveCard(card, tier, pos);
  }

  async function promoteToNow(item) {
    await moveCard({
      id: item.ticketId, num: item.num, title: item.title, repoName: item.repoName,
      priority: item.priority, status: item.status, prodIncident: item.prodIncident,
      assigneeLogin: item.assigneeLogin, assigneeName: item.assigneeName, sourceUrl: item.sourceUrl,
      tier: item.currentTier ?? 'later',
    }, 'now', null);
    loadAiTop();
  }

  const limits = board?.limits || { now: 10, next: 30 };

  return (
    <div className="space-y-4">
      {overviewOpen && (
        <Card className="border-primary/20 bg-primary/5">
          <CardContent className="pt-6">
            <h2 className="text-2xl font-semibold tracking-tight">Developers</h2>
            <p className="mt-1 text-sm text-muted-foreground">What ships next — a curated Now / Next / Later board over the dev backlog, with an AI-ranked hot list. Drag cards between tiers to reprioritize.</p>
          </CardContent>
        </Card>
      )}

      {/* Board / Backlog toggle — from the old DevPriorityView. */}
      <div className="flex flex-wrap items-center gap-2">
        <div className="inline-flex gap-1 rounded-lg bg-muted p-1">
          {[['board', 'Board'], ['backlog', 'Backlog']].map(([id, label]) => (
            <button key={id} onClick={() => setMode(id)}
              className={cn('rounded-md px-3 py-1.5 text-sm font-medium transition-colors', mode === id ? 'bg-background text-foreground shadow' : 'text-muted-foreground hover:text-foreground')}>{label}</button>
          ))}
        </div>
      </div>

      {error && (
        <div className="flex items-center justify-between gap-2 rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
          <span>{error}</span><button className="text-xs underline" onClick={() => setError(null)}>dismiss</button>
        </div>
      )}

      {mode === 'backlog' ? (
        <DevBacklog onOpen={setOpenItem} />
      ) : (
      <>
      {/* Nested tabs inside Board: What's hot (AI) first, then the kanban. */}
      <div className="inline-flex gap-1 rounded-lg bg-muted p-1">
        {[['hot', "What's hot right now"], ['kanban', 'Kanban board']].map(([id, label]) => (
          <button key={id} onClick={() => setBoardTab(id)}
            className={cn('rounded-md px-3 py-1.5 text-sm font-medium transition-colors', boardTab === id ? 'bg-background text-foreground shadow' : 'text-muted-foreground hover:text-foreground')}>{label}</button>
        ))}
      </div>

      {boardTab === 'hot' ? (
        <AiPriorityCard data={aiTop} busy={aiBusy} error={aiError} isTriager={isTriager} open={true}
          onToggle={() => {}} onRefresh={refreshAiTop} onPromote={promoteToNow}
          onOpen={(item) => setOpenItem(devToItem(item))} />
      ) : (
      <>
      {!isTriager && <p className="text-xs italic text-muted-foreground">You're viewing the board read-only. Triagers can move cards.</p>}

      {Array.isArray(board?.repoCounts) && board.repoCounts.length > 0 && (
        <DevBoardRepoFilter repos={board.repoCounts} active={repoFilterId} onChange={setRepoFilterId} />
      )}

      {loading && !board ? (
        <div className="rounded-xl border border-dashed p-10 text-center text-sm text-muted-foreground">Loading board…</div>
      ) : !board ? null : (
        <div className="grid gap-4 md:grid-cols-3">
          {DEV_TIERS.map(t => {
            const cards = board[t.id] || [];
            const filtered = repoFilterId != null;
            const tierGlobalTotal = t.id === 'later' ? (board.laterTotal ?? cards.length) : (board.tierTotals?.[t.id] ?? cards.length);
            const limit = t.id === 'now' ? limits.now : t.id === 'next' ? limits.next : null;
            const overLimit = limit != null && tierGlobalTotal > limit;
            const atLimit = limit != null && tierGlobalTotal === limit;
            const active = dropTarget?.tier === t.id;
            // Later paginates server-side (10-window); Now/Next paginate the
            // loaded cards client-side at 10/page. Drag positions use the
            // absolute index so within-column reordering stays correct.
            const isLater = t.id === 'later';
            const cpCount = Math.max(1, Math.ceil(cards.length / PER_PAGE));
            const cp = isLater ? 1 : Math.min(colPages[t.id] || 1, cpCount);
            const base = isLater ? 0 : (cp - 1) * PER_PAGE;
            const shown = isLater ? cards : cards.slice(base, base + PER_PAGE);
            return (
              <div key={t.id} onDragOver={e => onDragOverCol(e, t.id)} onDrop={e => onDrop(e, t.id)}
                className={cn('flex flex-col rounded-xl border bg-muted/40 transition-colors', active && 'ring-1 ring-primary')}>
                <div className="rounded-t-xl border-b border-t-2 px-3 py-2.5" style={{ borderTopColor: t.color }}>
                  <div className="flex items-center justify-between">
                    <div>
                      <div className="text-sm font-semibold">{t.label}</div>
                      <div className="text-[11px] text-muted-foreground">{t.sub}</div>
                    </div>
                    <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
                      {t.id === 'later'
                        ? <span>{cards.length}{tierGlobalTotal > cards.length && ` · of ${tierGlobalTotal}`}</span>
                        : <span>{tierGlobalTotal}{limit ? `/${limit}` : ''}{filtered && cards.length !== tierGlobalTotal && ` · ${cards.length} shown`}</span>}
                      {atLimit && <Badge variant="warning">at limit</Badge>}
                      {overLimit && <Badge variant="destructive">over</Badge>}
                    </div>
                  </div>
                </div>
                <div className="flex min-h-[120px] flex-1 flex-col gap-2 p-2">
                  {cards.length === 0 && (
                    <div className="flex flex-1 items-center justify-center p-4 text-center text-xs italic text-muted-foreground">
                      {filtered ? 'No matches in this repo.' : t.id === 'now' ? 'Nothing in flight. Promote items from Next.' : t.id === 'next' ? 'Empty queue. Pull from Later.' : 'No open dev tickets.'}
                    </div>
                  )}
                  {shown.map((c, idx) => (
                    <DevBoardCard key={c.id} card={c} isTriager={isTriager} busy={busy}
                      dragging={dragId === c.id}
                      onOpen={() => setOpenItem(devToItem(c))}
                      onDragStart={() => onDragStart(c)}
                      onDragOver={e => onDragOverCard(e, t.id, base + idx)}
                      onMoveTo={tier => moveCard(c, tier, null)} />
                  ))}
                </div>
                {!isLater && cards.length > PER_PAGE && (
                  <div className="flex items-center justify-between border-t px-3 py-2 text-xs text-muted-foreground">
                    <button className="disabled:opacity-40" disabled={cp <= 1} onClick={() => setColPages(p => ({ ...p, [t.id]: cp - 1 }))}>‹ Prev</button>
                    <span>{cp}/{cpCount}</span>
                    <button className="disabled:opacity-40" disabled={cp >= cpCount} onClick={() => setColPages(p => ({ ...p, [t.id]: cp + 1 }))}>Next ›</button>
                  </div>
                )}
                {isLater && tierGlobalTotal > DEV_LATER_PAGE && (
                  <div className="flex items-center justify-between border-t px-3 py-2 text-xs text-muted-foreground">
                    <button className="disabled:opacity-40" disabled={laterOffset === 0} onClick={() => setLaterOffset(Math.max(0, laterOffset - DEV_LATER_PAGE))}>‹ Newer</button>
                    <span>{laterOffset + 1}–{Math.min(laterOffset + DEV_LATER_PAGE, tierGlobalTotal)} of {tierGlobalTotal}</span>
                    <button className="disabled:opacity-40" disabled={laterOffset + DEV_LATER_PAGE >= tierGlobalTotal} onClick={() => setLaterOffset(laterOffset + DEV_LATER_PAGE)}>Older ›</button>
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
      </>
      )}
      </>
      )}

      {openItem && <TicketDetailModal item={openItem} onClose={() => setOpenItem(null)} />}
    </div>
  );
}
