import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Sidebar from './components/Sidebar';
import TerminalPane from './components/TerminalPane';
import FilesPane from './components/FilesPane';
import SettingsView from './components/SettingsView';
import NewSession from './components/NewSession';
import LayoutPicker from './components/LayoutPicker';
import Logo from './components/Logo';
import Overview from './components/Overview';
import Locked from './components/Locked';
import Welcome from './components/Welcome';
import * as api from './api';
import type { Cli, GridSpec, MoveTarget, OverviewFilter, Session, Tree } from './types';
import { GridGlyph, ListGlyph } from './components/icons';

// Phone-sized viewport: the app becomes two full-screen views (list ⇄ pane).
function useIsMobile() {
  const [m, setM] = useState(() => window.matchMedia('(max-width: 720px)').matches);
  useEffect(() => {
    const mq = window.matchMedia('(max-width: 720px)');
    const h = (e: MediaQueryListEvent) => setM(e.matches);
    mq.addEventListener('change', h);
    return () => mq.removeEventListener('change', h);
  }, []);
  return m;
}

// Auto layout: the grid grows to fit however many agents the group has.
function autoGrid(n: number): GridSpec {
  if (n <= 1) return { cols: 1, rows: 1 };
  if (n === 2) return { cols: 2, rows: 1 };
  if (n <= 4) return { cols: 2, rows: 2 };
  if (n <= 6) return { cols: 3, rows: 2 };
  return { cols: 3, rows: 3 };
}

type SettingsPage = 'general' | 'usage' | 'skills';
const ROOT_PATH = '.';
const normalizePath = (p?: string | null) => (p && p.trim() ? p : ROOT_PATH);

function initialTheme(): 'light' | 'dark' {
  const stored = localStorage.getItem('am-theme');
  if (stored === 'light' || stored === 'dark') return stored;
  return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

export default function App() {
  const [clis, setClis] = useState<Cli[]>([]);
  const [tree, setTree] = useState<Tree>({ order: [], groups: [], sessions: [] });
  const [activeRef, setActiveRef] = useState<string | null>(null);
  const [focusedId, setFocusedId] = useState<string | null>(null);
  const [theme, setTheme] = useState<'light' | 'dark'>(initialTheme);
  const [dropMain, setDropMain] = useState(false);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [settingsPage, setSettingsPage] = useState<SettingsPage>('general');
  // First-run welcome: null until /api/info loads; true when reopened manually.
  const [showWelcome, setShowWelcome] = useState(false);
  // Transient error toast for failed mutations (create/delete/move/…).
  const [toast, setToast] = useState<string | null>(null);
  const showErr = (msg: string) => (e: unknown) => { console.error(msg, e); setToast(msg); window.setTimeout(() => setToast(null), 4000); };
  // Overview presentation: tiles (default) or the classic list.
  const [ovView, setOvViewRaw] = useState<'tiles' | 'list'>(() =>
    (localStorage.getItem('am-ov-view') === 'list' ? 'list' : 'tiles'));
  const setOvView = (v: 'tiles' | 'list') => { setOvViewRaw(v); localStorage.setItem('am-ov-view', v); };
  // Archiving: sessions quiet for longer than the configured window are hidden
  // from the sidebar and overview unless "archived" is checked. Derived, never
  // stored — flipping the setting instantly (un)archives.
  const [showArchived, setShowArchived] = useState(false);
  const [archiveAfter, setArchiveAfter] = useState<'week' | 'month' | 'never'>('month');
  const [zoom, setZoom] = useState<number>(() => {
    const z = parseInt(localStorage.getItem('am-zoom') || '100', 10);
    return Number.isFinite(z) ? z : 100;
  });
  const [info, setInfo] = useState<Awaited<ReturnType<typeof api.getInfo>> | null>(null);
  // Default location for the next agent = where the last one was created,
  // falling back to the workspaces root.
  const [lastPath, setLastPath] = useState(() => normalizePath(localStorage.getItem('am-last-path')));
  // Mobile navigation: false = the sidebar is the (full-screen) home view,
  // true = the selected session/group fills the screen. Desktop ignores this.
  const isMobile = useIsMobile();
  const [mobileStage, setMobileStage] = useState(false);
  const [ovFilter, setOvFilter] = useState<OverviewFilter>('all');
  const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
  const rememberPath = (p?: string | null) => {
    const next = normalizePath(p);
    setLastPath(next);
    localStorage.setItem('am-last-path', next);
  };

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem('am-theme', theme);
  }, [theme]);

  // Refresh info while locked so flipping the Space to Private unlocks the UI
  // without a manual reload (the server re-checks visibility every minute).
  useEffect(() => {
    api.getInfo().then(setInfo).catch(() => {});
    if (!info?.locked) return;
    const t = setInterval(() => api.getInfo().then(setInfo).catch(() => {}), 15_000);
    return () => clearInterval(t);
  }, [info?.locked]);
  useEffect(() => { localStorage.setItem('am-zoom', String(zoom)); }, [zoom]);

  // Show the welcome once, when /api/info first loads: on first run (never seen)
  // or whenever demo mode is active (so the Space reads like a fresh install).
  // Fires once so dismissing it during a demo doesn't make it re-pop.
  const welcomeBoot = useRef(false);
  useEffect(() => {
    if (!info || welcomeBoot.current) return;
    welcomeBoot.current = true;
    if (!info.locked && (info.welcomeSeen === false || info.demoMode)) setShowWelcome(true);
  }, [info]);
  const dismissWelcome = () => {
    setShowWelcome(false);
    setInfo((i) => (i ? { ...i, welcomeSeen: true } : i));
    api.dismissWelcome().catch(() => {});
  };
  const openWelcome = () => { setSettingsOpen(false); setShowWelcome(true); };
  // Demo mode: hide current sessions from view (nothing is deleted). Toggling
  // off restores the full sidebar. Turning it on shows the welcome; off hides it.
  const toggleDemo = async () => {
    const next = !info?.demoMode;
    try {
      const r = await api.setDemo(next);
      setInfo((i) => (i ? { ...i, demoMode: r.active } : i));
      setSettingsOpen(false);
      await refresh();
      setShowWelcome(next);
    } catch (e) { showErr('Couldn’t toggle demo mode')(e); }
  };

  const refresh = useCallback(async () => {
    try { setTree(await api.getTree()); } catch { /* offline */ }
  }, []);

  useEffect(() => {
    api.getClis().then(setClis).catch(() => {});
    refresh();
    // Skip polling while the tab is hidden; catch up immediately on return.
    const t = setInterval(() => { if (!document.hidden) refresh(); }, 2500);
    const onVisible = () => { if (!document.hidden) refresh(); };
    document.addEventListener('visibilitychange', onVisible);
    return () => { clearInterval(t); document.removeEventListener('visibilitychange', onVisible); };
  }, [refresh]);

  // Last-activity ages for the sidebar clock column (from the trace digests;
  // low-frequency — the Overview does its own faster polling when open).
  const [ages, setAges] = useState<Record<string, number>>({});
  useEffect(() => {
    let alive = true;
    const load = () => api.getMeta()
      .then((r) => {
        if (!alive) return;
        setAges(Object.fromEntries(r.sessions.map((s) => [
          s.id, Math.max(s.digest?.lastAssistantTs || 0, s.digest?.lastPromptTs || 0),
        ])));
      })
      .catch(() => {});
    load();
    const t = setInterval(() => { if (!document.hidden) load(); }, 20_000);
    return () => { alive = false; clearInterval(t); };
  }, []);

  // Archive threshold from the operator config; refresh when settings closes
  // (that's where it's edited).
  useEffect(() => {
    if (settingsOpen) return;
    api.getConfig().then((c) => setArchiveAfter(c.archive?.after ?? 'month')).catch(() => {});
  }, [settingsOpen]);
  const archivedIds = useMemo(() => {
    const out = new Set<string>();
    if (archiveAfter === 'never') return out;
    const cut = Date.now() - (archiveAfter === 'week' ? 7 : 30) * 864e5;
    for (const s of tree.sessions) {
      // Shells and file panes have no trace clock — never archive them.
      if (s.cli === 'shell' || s.cli === 'files' || s.state === 'working') continue;
      const last = ages[s.id] || Date.parse(s.createdAt) || 0;
      if (last && last < cut) out.add(s.id);
    }
    return out;
  }, [tree.sessions, ages, archiveAfter]);

  const cliMap = useMemo(() => Object.fromEntries(clis.map((c) => [c.id, c])), [clis]);
  const sessById = useMemo(() => Object.fromEntries(tree.sessions.map((s) => [s.id, s])), [tree.sessions]);
  const groupById = useMemo(() => Object.fromEntries(tree.groups.map((g) => [g.id, g])), [tree.groups]);

  // Keep a valid selection ('overview' is always valid).
  useEffect(() => {
    const ok = activeRef && (activeRef === 'overview'
      || (activeRef.startsWith('g:') ? groupById[activeRef.slice(2)] : sessById[activeRef.slice(2)]));
    if (!ok) setActiveRef(tree.order[0] ?? null);
  }, [tree.order, groupById, sessById, activeRef]);

  const activeGroup = activeRef?.startsWith('g:') ? groupById[activeRef.slice(2)] : null;
  const activeSingle = activeRef?.startsWith('s:') ? sessById[activeRef.slice(2)] : null;
  const groupSessions = useMemo(
    () => (activeGroup ? activeGroup.sessionIds.map((id) => sessById[id]).filter(Boolean) as Session[] : []),
    [activeGroup, sessById],
  );

  // Tile grid for the active group: the chosen layout, or auto (fit the count).
  // On mobile it's always a single pane — the chip strip switches between them.
  const grid: GridSpec = activeGroup && !isMobile ? (activeGroup.layout ?? autoGrid(groupSessions.length)) : { cols: 1, rows: 1 };
  const cap = grid.cols * grid.rows;
  const pageCount = Math.max(1, Math.ceil(groupSessions.length / cap));
  // Page resets happen explicitly in the navigation handlers (an effect on
  // activeRef would clobber openSession's "land on this agent's pane").
  const [pageRaw, setPage] = useState(0);
  const page = Math.min(pageRaw, pageCount - 1); // clamp when agents/layout change
  const pageSessions = activeGroup ? groupSessions.slice(page * cap, (page + 1) * cap) : [];

  const visibleSessions = activeGroup ? pageSessions : activeSingle ? [activeSingle] : [];
  const visibleIds = visibleSessions.map((s) => s.id).join(',');
  const showZoom = visibleSessions.length > 0;

  // Pane rearrangement (drag a pane header onto another tile).
  const [paneDrag, setPaneDrag] = useState(false);
  const [overTile, setOverTile] = useState<number | null>(null);
  // A sidebar row being dragged ("s:<id>" / "g:<id>") — the stage shows
  // per-cell drop targets for sessions instead of one big outline.
  const [sessionDrag, setSessionDrag] = useState<string | null>(null);
  const sessionDragActive = !!sessionDrag?.startsWith('s:');

  // Keep a focused pane within the visible set.
  useEffect(() => {
    const ids = visibleIds ? visibleIds.split(',') : [];
    if (!focusedId || !ids.includes(focusedId)) setFocusedId(ids[0] ?? null);
  }, [visibleIds, focusedId]);

  // actions
  const createSession = async (name: string, cli: string, path: string, groupId?: string) => {
    try {
      const s = await api.createSession(name, cli, groupId, path);
      rememberPath(s.path);
      await refresh();
      if (groupId) { setActiveRef(`g:${groupId}`); setFocusedId(s.id); }
      else setActiveRef(`s:${s.id}`);
    } catch (e) { showErr('Couldn’t create the agent')(e); }
  };
  // Quickstart: server boots the agent and types the prompt; we jump straight
  // to the new pane so you watch it happen.
  const quickStart = async (cli: string, prompt: string, name = '', path = '.') => {
    try {
      const s = await api.quickStart(cli, prompt, name, path);
      rememberPath(s.path);
      await refresh();
      setActiveRef(`s:${s.id}`);
    } catch (e) { showErr('Couldn’t quickstart the agent')(e); }
  };
  // Creations land in an explicitly targeted group (the group's + button),
  // else the group you're currently looking at; loose otherwise.
  const newSession = (name: string, cli: string, path: string, groupId?: string) =>
    createSession(name, cli, path, groupId ?? activeGroup?.id);
  const newGroup = async (name: string, cart?: { cli: string; count: number }[], path = ROOT_PATH) => {
    try {
      const g = await api.createGroup(name);
      for (const { cli, count } of cart || []) {
        const base = cliMap[cli]?.label || cli;
        for (let i = 0; i < count; i++) {
          const s = await api.createSession(count > 1 ? `${base} ${i + 1}` : base, cli, g.id, path);
          rememberPath(s.path);
        }
      }
      await refresh();
      setActiveRef(`g:${g.id}`);
    } catch (e) { showErr('Couldn’t create the group')(e); }
  };
  const doMove = (ref: string, to: MoveTarget) => api.move(ref, to).then(refresh).catch(showErr('Couldn’t move that'));
  const renameGroup = (id: string, name: string) => api.renameGroup(id, name).then(refresh).catch(showErr('Couldn’t rename'));
  const renameSession = (id: string, name: string) => { if (name.trim()) api.renameSession(id, name.trim()).then(refresh).catch(showErr('Couldn’t rename')); };
  const deleteGroup = (id: string) => api.deleteGroup(id).then(() => { if (activeRef === `g:${id}`) setActiveRef(null); refresh(); }).catch(showErr('Couldn’t delete the group'));
  const stopSession = (id: string) => api.stopSession(id).then(refresh).catch(showErr('Couldn’t stop the agent'));
  const deleteSession = (id: string) => api.deleteSession(id).then(() => { if (activeRef === `s:${id}`) setActiveRef(null); refresh(); }).catch(showErr('Couldn’t delete the agent'));
  // Clicking a session: nested → open its group with that pane focused; loose → solo view.
  const openSession = (sid: string, groupId?: string) => {
    if (groupId) {
      setActiveRef(`g:${groupId}`);
      setFocusedId(sid);
      // Land on the page that actually contains this agent's pane.
      const g = groupById[groupId];
      const idx = g?.sessionIds.indexOf(sid) ?? -1;
      const gg = isMobile ? { cols: 1, rows: 1 } : (g?.layout ?? autoGrid(g?.sessionIds.length ?? 1));
      setPage(idx >= 0 ? Math.floor(idx / (gg.cols * gg.rows)) : 0);
    } else {
      setActiveRef(`s:${sid}`);
      setPage(0);
    }
    if (isMobile) setMobileStage(true);
  };
  const activate = (ref: string) => {
    setActiveRef(ref);
    setPage(0);
    if (isMobile) setMobileStage(true);
  };
  const closePane = (sid: string) => {
    // On mobile ✕ just returns to the list — never the desktop ungroup gesture.
    if (isMobile) { setMobileStage(false); return; }
    if (activeGroup) doMove(`s:${sid}`, { kind: 'after', ref: activeRef! });
    else setActiveRef(null);
  };
  const setLayout = async (l: GridSpec | null) => {
    if (!activeGroup) return;
    await api.updateGroup(activeGroup.id, { layout: l });
    refresh();
  };
  // Drop pane `paneId` on the tile showing position `targetIdx` (absolute index
  // into the group's order): occupied tile → swap places; empty tile → move to
  // the end.
  const movePane = async (paneId: string, targetIdx: number) => {
    if (!activeGroup) return;
    const ids = activeGroup.sessionIds.slice();
    const from = ids.indexOf(paneId);
    if (from < 0) return;
    const target = groupSessions[targetIdx];
    if (target && target.id !== paneId) {
      const ti = ids.indexOf(target.id);
      ids[from] = ids[ti];
      ids[ti] = paneId;
    } else if (!target) {
      ids.splice(from, 1);
      ids.push(paneId);
    } else return;
    await api.updateGroup(activeGroup.id, { sessionIds: ids });
    refresh();
  };

  const promptUser = info?.spaceId?.split('/')[0] || 'you';
  // Empty states speak the app's native language: a prompt, waiting.
  const EmptyPrompt = ({ path, hints }: { path: string; hints: string[] }) => (
    <div className="empty-term">
      <div className="et-prompt mono">
        <span className="et-user">{promptUser}</span>
        <span className="et-dim">/</span>
        <span className="et-path">{path}</span>
        <span className="et-dim"> $</span>
        <span className="et-cursor" />
      </div>
      {hints.map((h) => <p key={h} className="et-hint">{h}</p>)}
    </div>
  );

  const allowDrop = (e: React.DragEvent) => { if (paneDrag) return; e.preventDefault(); setDropMain(true); };
  const onDropMain = (e: React.DragEvent) => {
    e.preventDefault();
    const ref = e.dataTransfer.getData('text/plain');
    if (ref?.startsWith('s:') && activeGroup) doMove(ref, { kind: 'into', groupId: activeGroup.id });
    setDropMain(false);
  };

  // One grid cell. Empty cells become visible drop targets while a pane drags
  // (rearrange) or a sidebar session drags over the group (join at that spot).
  const tileDnd = (i: number, occupied: boolean) => ({
    onDragOver: (e: React.DragEvent) => {
      if (paneDrag || (sessionDragActive && !occupied)) { e.preventDefault(); e.stopPropagation(); setOverTile(i); }
    },
    onDragLeave: () => setOverTile((t) => (t === i ? null : t)),
    onDrop: (e: React.DragEvent) => {
      const d = e.dataTransfer.getData('text/plain');
      if (d.startsWith('p:')) { e.preventDefault(); e.stopPropagation(); movePane(d.slice(2), page * cap + i); }
      else if (d.startsWith('s:') && activeGroup) { e.preventDefault(); e.stopPropagation(); doMove(d, { kind: 'into', groupId: activeGroup.id }); }
      setOverTile(null);
    },
  });

  const renderTiles = (sessions: Session[], g: GridSpec) => {
    const slotCount = activeGroup ? g.cols * g.rows : sessions.length;
    const slots = Array.from({ length: slotCount }, (_, i) => sessions[i] ?? null);
    const canDrag = !!activeGroup && groupSessions.length > 1;
    // A sidebar session hovering a full grid still needs somewhere to land:
    // offer a ghost strip appended below the tiles.
    const ghost = sessionDragActive && !!activeGroup;
    return (
      <div
        className={`tiles${paneDrag ? ' pane-dragging' : ''}${sessionDragActive && activeGroup ? ' session-dragging' : ''}`}
        style={{ gridTemplateColumns: `repeat(${g.cols}, 1fr)`, gridTemplateRows: `repeat(${g.rows}, minmax(0, 1fr))` }}
        onDragOver={activeGroup ? allowDrop : undefined}
        onDragLeave={() => setDropMain(false)}
        onDrop={activeGroup ? onDropMain : undefined}
      >
        {slots.map((s, i) => (
          <div
            key={s ? s.id : `empty-${i}`}
            className={`tile${s ? '' : ' tile-empty'}${(paneDrag || sessionDragActive) && overTile === i ? ' tile-over' : ''}`}
            {...(activeGroup ? tileDnd(i, !!s) : {})}
          >
            {s && (s.cli === 'files' ? (
              <FilesPane
                session={s}
                zoom={zoom}
                focused={visibleSessions.length > 1 && s.id === focusedId}
                dragId={canDrag ? `p:${s.id}` : undefined}
                onDragActive={setPaneDrag}
                onFocus={() => setFocusedId(s.id)}
                onClose={() => closePane(s.id)}
              />
            ) : (
              <TerminalPane
                session={s}
                cli={cliMap[s.cli]}
                theme={theme}
                zoom={zoom}
                focused={visibleSessions.length > 1 && s.id === focusedId}
                active={s.id === focusedId}
                dragId={canDrag ? `p:${s.id}` : undefined}
                onDragActive={setPaneDrag}
                onFocus={() => setFocusedId(s.id)}
                onRename={(name) => renameSession(s.id, name)}
                onClose={() => closePane(s.id)}
              />
            ))}
          </div>
        ))}
        {ghost && (
          <div
            className={`tile tile-ghost${overTile === slots.length ? ' tile-over' : ''}`}
            {...tileDnd(slots.length, false)}
          >
            drop to add to {activeGroup!.name}
          </div>
        )}
      </div>
    );
  };

  if (info?.locked) return <Locked spaceId={info.spaceId} reason={info.lockReason} bucket={info.lockBucket} />;

  if (settingsOpen) {
    return (
      <SettingsView
        page={settingsPage}
        onPage={setSettingsPage}
        onClose={() => setSettingsOpen(false)}
        theme={theme}
        onToggleTheme={toggleTheme}
        clis={clis}
        info={info}
        onShowWelcome={openWelcome}
        demoMode={!!info?.demoMode}
        onToggleDemo={toggleDemo}
      />
    );
  }

  return (
    <div className={`app${isMobile ? (mobileStage ? ' m-stage' : ' m-home') : ''}`}>
      {showWelcome && <Welcome onClose={dismissWelcome} />}
      {toast && <div className="toast mono" role="alert">{toast}</div>}
      <Sidebar
        clis={clis}
        tree={tree}
        activeRef={activeRef}
        focusedId={focusedId}
        defaultPath={lastPath}
        ages={ages}
        onActivate={activate}
        onOpenSession={openSession}
        onNewSession={newSession}
        onNewGroup={newGroup}
        onRenameGroup={renameGroup}
        onRenameSession={renameSession}
        onDeleteGroup={deleteGroup}
        onStopSession={stopSession}
        onDeleteSession={deleteSession}
        onMove={doMove}
        onDragState={setSessionDrag}
        onOpenSettings={() => setSettingsOpen(true)}
        theme={theme}
        onToggleTheme={toggleTheme}
        onQuickStart={quickStart}
        archived={archivedIds}
        showArchived={showArchived}
        onToggleArchived={() => setShowArchived((v) => !v)}
      />

      <div className="main">
        {isMobile && mobileStage && (
          <div className="mbar">
            <button className="icon-btn mback" onClick={() => setMobileStage(false)} title="Back to list">‹</button>
            {activeGroup ? (
              <div className="mchips">
                {groupSessions.map((s, i) => (
                  <button key={s.id} className={`mchip${i === page ? ' on' : ''}`} title={s.name} onClick={() => setPage(i)}>
                    <Logo cli={s.cli} size={13} tint={cliMap[s.cli]?.color} />
                    <span className={`status ${s.state}`} />
                  </button>
                ))}
              </div>
            ) : (
              <span className="mtitle mono">{activeRef === 'overview' ? 'Overview' : activeSingle?.name}</span>
            )}
          </div>
        )}
        <div className="stage">
          {activeRef === 'overview' ? (
            <Overview
              clis={clis}
              tree={tree}
              filter={ovFilter}
              view={ovView}
              archived={archivedIds}
              showArchived={showArchived}
              onOpen={(sid) => {
                const g = tree.groups.find((x) => x.sessionIds.includes(sid));
                openSession(sid, g?.id);
              }}
            />
          ) : activeGroup ? (
            groupSessions.length === 0 ? (
              <div
                className={`empty-group${dropMain ? ' drop-over' : ''}`}
                onDragOver={allowDrop}
                onDragLeave={() => setDropMain(false)}
                onDrop={onDropMain}
              >
                <div className="empty-card">
                  <EmptyPrompt path={activeGroup.name} hints={['create an agent here, or drag one in from the sidebar']} />
                  <NewSession clis={clis} sessions={tree.sessions} defaultPath={lastPath} onCreate={newSession} />
                  <div className="dropline">drop an agent here to add it to this group</div>
                </div>
              </div>
            ) : renderTiles(pageSessions, grid)
          ) : activeSingle ? (
            renderTiles([activeSingle], { cols: 1, rows: 1 })
          ) : (
            <div className="empty-group">
              <EmptyPrompt
                path="workspaces"
                hints={['create an agent or a group from the sidebar', 'drag an agent onto another to group them']}
              />
            </div>
          )}
        </div>
        {activeRef === 'overview' && (
          <div className="zoombar ov-bar">
            <div className="seg ov-seg">
              {(['all', 'waiting', 'working', 'quiet'] as OverviewFilter[]).map((f) => (
                <button key={f} className={ovFilter === f ? 'on' : ''} onClick={() => setOvFilter(f)}>
                  {f === 'waiting' ? 'done' : f === 'working' ? 'running' : f === 'quiet' ? 'stopped' : 'all'}
                </button>
              ))}
            </div>
            <div className="seg ov-seg ov-viewseg">
              <button className={ovView === 'tiles' ? 'on' : ''} title="Tiles" onClick={() => setOvView('tiles')}><GridGlyph /></button>
              <button className={ovView === 'list' ? 'on' : ''} title="List" onClick={() => setOvView('list')}><ListGlyph /></button>
            </div>
          </div>
        )}
        {showZoom && (
          <div className="zoombar">
            {!isMobile && activeGroup && groupSessions.length > 0 && (
              <LayoutPicker grid={grid} isAuto={!activeGroup.layout} onPick={setLayout} />
            )}
            {!isMobile && activeGroup && pageCount > 1 && (
              <span className="pager">
                <button className="zbtn" title="Previous panes" disabled={page === 0} onClick={() => setPage(page - 1)}>‹</button>
                <span className="plbl mono">{page + 1}/{pageCount}</span>
                <button className="zbtn" title="Next panes" disabled={page >= pageCount - 1} onClick={() => setPage(page + 1)}>›</button>
              </span>
            )}
            <span className="spacer" />
            <button className="zbtn" title="Zoom out" onClick={() => setZoom((z) => Math.max(50, z - 10))}>−</button>
            <button className="zlvl" title="Reset to 100%" onClick={() => setZoom(100)}>{zoom}%</button>
            <button className="zbtn" title="Zoom in" onClick={() => setZoom((z) => Math.min(200, z + 10))}>+</button>
          </div>
        )}
      </div>
    </div>
  );
}
