import { PRODUCT_IDENTITY, WORKSPACE_STORAGE_TYPE } from '@learnitive/constants';
import PUBLIC_ROUTE_METADATA from '../../../../shared/constants/public-route-metadata.json';
import { upsertMeta, usePageMetadata, type PageMetadata } from './page-metadata';
import { NavigationController, NavigationProgress, RouteLoadingSkeleton } from './navigation-progress';
import { resolveTechnologyIcon, type TechnologyIconSpec } from './technology-icon-registry';
import {
  CoreServiceProvider,
  LoggerProvider,
  type Services,
  useCoreServices,
} from '@learnitive/context';
import type { Logger } from '@learnitive/logger';
import { hasPermission } from '@learnitive/native-fs';
import {
  createDefaultLearnitiveProjectConfig,
  fetchProjectTemplateFiles,
  loadProjectTemplateManifest,
  loadProjectTemplateCategories,
  loadProjectTemplateInstitutions,
  normalizeProjectSlug,
  projectFilePath,
  requestPersistentBrowserStorage,
  supportsOpfsProjectStorage,
  writePreparedProjectTemplateFiles,
  type ProjectEntry,
  type ProjectStorageProvider,
  type ProjectTemplateCategory,
  type ProjectTemplateDefinition,
  type ProjectTemplateInstitution,
} from '@learnitive/project-storage';
import {
  canonicalProfileForMode,
  PROJECT_MODE_CATALOG,
  PROJECT_STARTERS,
  projectLanguageById,
  isProjectProfile,
  projectModeById,
  projectModeForProfile,
  type ProjectProfile,
} from '@learnitive/project-runtime';
import type {
  RootEmitter,
  Store,
  WorkspaceInfo,
  WorkspaceStorageType,
} from '@learnitive/types';
import {
  Button,
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  Input,
  Label,
  Toaster,
  toast,
} from '@learnitive/ui-components';
import { WsPath } from '@learnitive/ws-path';
import { useAtomValue } from 'jotai';
import { Provider } from 'jotai/react';
import {
  Archive,
  ArrowRight,
  BookOpen,
  CalendarDays,
  Check,
  ChevronLeft,
  ChevronRight,
  Download,
  CircleUserRound,
  Code2,
  Copy,
  Crown,
  ExternalLink,
  FileCode2,
  FileText,
  FolderOpen,
  GitBranch,
  LayoutTemplate,
  LoaderCircle,
  LogOut,
  Menu,
  MessageSquareText,
  Moon,
  MoreHorizontal,
  Pencil,
  Plus,
  Search,
  Sparkles,
  Sun,
  TerminalSquare,
  Trash2,
  Upload,
  Users,
  X,
} from 'lucide-react';
import React from 'react';
import { AppErrorHandler } from '../app-error-handler';
import { ErrorBoundary } from '../components/feedback/error-boundary';
import { LearnitiveAccountPopover } from '../components/account-popover';
import { DismissibleDetails } from '../components/dismissible-popover';
import { PageModalHost, requestPageConfirm, requestPagePrompt } from '../components/page-modal-service';
import { AppDialogs } from '../dialogs/app-dialogs';
import { CloneGitRepositoryDialog } from '../components/clone-git-repository-dialog';
import { consumeLearnExample } from '../learn/runtime-handoff';
import { AdminLearnCourseList } from '../learn/admin/course-list';
import { AdminLearnCategoryList } from '../learn/admin/category-list';
import { AdminLearnImportPage } from '../learn/admin/import-page';
import { AdminLearnPreviewPage } from '../learn/admin/preview-page';
import { listLearnCourses } from '../learn/api';
import { AdminLearnCourseBuilder } from '../learn/admin/course-builder';
import { safeDownloadName, saveZipStream } from '../components/project-zip-actions';
import { HomeHero } from '../components/public-home/home-hero';
import { HomeAiBuilder } from '../components/public-home/home-ai-builder';
import { HeroCloudTerminal } from '../components/public-home/hero-cloud-terminal';
import { HomeWorkflowsShowcase } from '../components/public-home/home-workflows-showcase';
import { HomeCommunityReviews } from '../components/public-home/home-community-reviews';
import {
  CompareDetailPageContent,
  CompareIndexPageContent,
  FeaturesPageContent,
  HOME_FAQ,
  HomeProductComparison,
  PRICING_FAQ,
  PricingComparisonTable,
  PublicFaq,
  PublicFinalCta,
  LearnitivePricingTable,
} from './public-marketing';
import { InteractiveAuthCard, safeClientReturnPath } from './auth-card';
import { CmsInteractiveContent, interactiveContentToc } from '../interactive-content/cms-renderer';
import { AdminCmsEditorPage, AdminCmsListPage } from './admin-cms-pages';
import { SaveProtection } from '../save-protection';
import { ProjectMirrorCoordinator } from './project-mirror-coordinator';
import { ProjectStorageDialog } from './project-storage-dialog';
import { PROJECTS_FOLDER_STATE_EVENT, ProjectMirrorService, finalizeCreatedBrowserProject } from './project-mirror-service';
import {
  adminChangePassword,
  adminClearInstitutionLogo,
  adminGet,
  adminLogin,
  adminMutate,
  adminUploadInstitutionLogo,
  adminUploadTemplate,
  adminReplaceTemplate,
  adminUploadTemplatePreview,
  adminClearTemplatePreview,
  adminUploadTemplateThumbnail,
  adminClearTemplateThumbnail,
  SaasApiError,
  discoverPublishedProjects,
  forkPublishedProject,
  deleteAccount,
  getAuthState,
  getAccountUsage,
  getCloudProject,
  listProjectSandboxProcesses,
  getSafeSettings,
  getBlog,
  getChangeLog,
  getLegalPage,
  getDoc,
  cmsGetInteractiveState,
  getPublicSaasConfig,
  listBlog,
  listBlogPage,
  listDocs,
  logout,
  consumeMagicLogin,
  getProjectSandboxPreviewAccess,
  redeemProjectShareToken,
  submitContact,
  updateProfile,
  updateSafeSettings,
  verifyAccountLicense,
  type AccountUsageDay,
  type AdminInstitutionRecord,
  type AdminTemplateCategoryRecord,
  type AdminTemplateRecord,
  type BlogTagSummary,
  type ChangeLogEvent,
  type ChangeLogPageSettings,
  type LegalPage as LegalPageRecord,
  type CloudProject,
  type DiscoveredProject,
  type ProjectPublication,
  type CmsDetail,
  type CmsListItem,
  type PublicSaasConfig,
  type SaasAuthState,
  type SaasEntitlement,
  type SaasUser,
} from './saas-api';
const LearnCatalogPage = React.lazy(() => import('../learn/public/learn-pages').then((module) => ({ default: module.LearnCatalogPage })));
const LearnCoursePage = React.lazy(() => import('../learn/public/learn-pages').then((module) => ({ default: module.LearnCoursePage })));
const LearnPlayerPage = React.lazy(() => import('../learn/public/learn-pages').then((module) => ({ default: module.LearnPlayerPage })));
const LearnCertificatePage = React.lazy(() => import('../learn/public/learn-pages').then((module) => ({ default: module.LearnCertificatePage })));

function LearnLazyBoundary({ children }: { children: React.ReactNode }) {
  return <React.Suspense fallback={<RouteLoadingSkeleton />}>{children}</React.Suspense>;
}

const LazyTemplateCategoryIcon = React.lazy(() => import('./template-category-icon').then((module) => ({ default: module.TemplateCategoryIcon })));
const TemplateCategoryIconBrowser = React.lazy(() => import('./template-category-icon-browser').then((module) => ({ default: module.TemplateCategoryIconBrowser })));
const LazyTemplatePdfPreview = React.lazy(() => import('./template-pdf-preview').then((module) => ({ default: module.TemplatePdfPreview })));

function TemplateCategoryIcon(props: { name?: string; className?: string }) {
  return <React.Suspense fallback={<FileText className={props.className || 'size-6'} aria-hidden="true"/>}><LazyTemplateCategoryIcon {...props}/></React.Suspense>;
}

function TemplatePdfPreview(props: { url: string; title: string; compact?: boolean }) {
  return <React.Suspense fallback={<div className="interactive-template-pdfjs-state">Loading PDF preview…</div>}><LazyTemplatePdfPreview {...props}/></React.Suspense>;
}

const AuthContext = React.createContext<{
  auth: SaasAuthState;
  config?: PublicSaasConfig;
  loading: boolean;
  refresh: () => Promise<void>;
}>({ auth: { authenticated: false }, loading: true, refresh: async () => {} });

function useSaasSession() {
  return React.useContext(AuthContext);
}

function ProjectMirrorCoordinatorHost({ rootEmitter }: { rootEmitter: RootEmitter }) {
  return <ProjectMirrorCoordinator rootEmitter={rootEmitter} />;
}

function SaasSessionProvider({ children }: { children: React.ReactNode }) {
  const core = useCoreServices();
  const [auth, setAuth] = React.useState<SaasAuthState>({ authenticated: false });
  const [config, setConfig] = React.useState<PublicSaasConfig>();
  const [loading, setLoading] = React.useState(true);
  const refresh = React.useCallback(async () => {
    const [nextConfig, nextAuth] = await Promise.all([
      getPublicSaasConfig().catch(() => undefined),
      getAuthState().catch(() => ({ authenticated: false } as const)),
    ]);
    setConfig(nextConfig);
    setAuth(nextAuth);
    if (nextAuth.authenticated) {
      // Account preferences are safe metadata. Restore them after login without
      // making local editor startup depend on the account service.
      const settings = await getSafeSettings().catch(() => undefined);
      if (settings?.theme && ['system', 'light', 'dark'].includes(settings.theme)) {
        core.workbenchState.changeThemePreference(settings.theme);
      }
    }
    setLoading(false);
  }, [core.workbenchState]);
  React.useEffect(() => { void refresh(); }, [refresh]);
  return <AuthContext.Provider value={{ auth, config, loading, refresh }}>{children}</AuthContext.Provider>;
}

export function SaasApp({
  logger,
  store,
  rootEmitter,
  services,
}: {
  logger: Logger;
  store: Store;
  rootEmitter: RootEmitter;
  services: Services;
}) {
  return (
    <LoggerProvider logger={logger}>
      <Provider store={store}>
        <CoreServiceProvider services={services.core}>
          <ErrorBoundary>
            <SaasSessionProvider>
              <PageModalHost />
              <AppDialogs />
              <Toaster position="top-right" />
              <AppErrorHandler rootEmitter={rootEmitter} />
              <SaveProtection />
              <ProjectMirrorCoordinatorHost rootEmitter={rootEmitter} />
              <NavigationController />
              <NavigationProgress />
              <SaasRoutes />
            </SaasSessionProvider>
          </ErrorBoundary>
        </CoreServiceProvider>
      </Provider>
    </LoggerProvider>
  );
}

function usePathname() {
  const [pathname, setPathname] = React.useState(() => window.location.pathname.replace(/\/+$/u, '') || '/');
  React.useEffect(() => {
    const listener = () => setPathname(window.location.pathname.replace(/\/+$/u, '') || '/');
    window.addEventListener('popstate', listener);
    window.addEventListener('learnitive:locationchange', listener);
    return () => { window.removeEventListener('popstate', listener); window.removeEventListener('learnitive:locationchange', listener); };
  }, []);
  return pathname;
}


function routeMetadata(pathname: string, searchParams = new URLSearchParams()): PageMetadata {
  const privateRoute = pathname === '/terminal' || pathname === '/appterminal' || pathname === '/projects' || pathname === '/explore' || pathname === '/settings' || pathname === '/apply' || pathname.startsWith('/join/') || pathname.startsWith('/share/') || pathname.startsWith('/admin') || pathname === '/login' || pathname === '/register' || pathname === '/auth/magic';
  const privateRows: Record<string, [string, string]> = {
    '/projects': ['Projects', 'Your Learnitive local projects and optional cloud metadata registry.'],
    '/explore': ['Explore', 'Browse authenticated published Learnitive project snapshots and fork them into your own local-first workspace.'],
    '/settings': ['Settings', 'Manage your Learnitive account and safe preferences.'],
    '/login': ['Login', 'Sign in to Learnitive with Google, GitHub or a secure email link.'],
    '/register': ['Register', 'Create an Learnitive account with Google, GitHub or a secure email link.'],
    '/auth/magic': ['Email sign in', 'Complete a secure one time Learnitive email sign in.'],
  };
  const shared = PUBLIC_ROUTE_METADATA[pathname as keyof typeof PUBLIC_ROUTE_METADATA];
  const exact: [string, string] | undefined = shared ? [shared.title, shared.description] : privateRows[pathname];
  if (exact) {
    const faceted = (pathname === '/templates' || pathname === '/blog') && [...searchParams.keys()].length > 0;
    const sharedNoIndex = Boolean(shared && 'noIndex' in shared && shared.noIndex);
    const noIndex = privateRoute || sharedNoIndex;
    return { title: exact[0], description: exact[1], pathname, noIndex, robots: faceted ? 'noindex,follow' : noIndex ? 'noindex,nofollow' : 'index,follow' };
  }
  if (pathname.startsWith('/tools/')) { const tool = PUBLIC_TOOLS.find((item) => `/tools/${item.slug}` === pathname); if (tool) return { title: tool.title, description: tool.summary, pathname }; }
  if (pathname === '/compare/overleaf') return { title: 'Learnitive vs Overleaf | Local-First Writing & Research', description: 'Compare Learnitive and Overleaf for LaTeX, local-first projects, research tools, AI and portable writing workflows.', pathname };
  if (pathname.startsWith('/compare/')) return { title: 'Page not found', description: 'This comparison is not part of the current Learnitive launch.', pathname, noIndex: true, robots: 'noindex,nofollow' };
  if (pathname.startsWith('/migrate/')) return { title: 'Page not found', description: 'This route is not part of the current Learnitive launch.', pathname, noIndex: true, robots: 'noindex,nofollow' };
    if (pathname.startsWith('/use-cases/')) { const item = ALL_USE_CASES.find((entry) => `/use-cases/${entry.slug}` === pathname); if (item) return { title: item.title, description: item.summary, pathname }; }
  if (pathname.startsWith('/try/')) return { title: 'Open Learning Workspace', description: 'Open a temporary Learnitive interactive learning workspace.', pathname, noIndex: true, robots: 'noindex,nofollow' };
  if (pathname.startsWith('/templates/categories/')) return { title: 'LaTeX Template Category', description: 'Browse an Learnitive LaTeX template category.', pathname, enabled: false };
  if (pathname.startsWith('/templates/')) return { title: 'Template', description: 'Preview and apply an Learnitive project template.', pathname, enabled: false };
  if (pathname.startsWith('/blog/')) return { title: 'Learnitive Blog', description: 'A published Learnitive article.', pathname, type: 'article', enabled: false };
  if (pathname.startsWith('/docs/')) return { title: 'Learnitive Docs', description: 'Learnitive documentation.', pathname, enabled: false };
  if (pathname.startsWith('/learn/')) return { title: 'Learn', description: 'An Learnitive interactive course or lesson.', pathname, enabled: false };
  if (pathname.startsWith('/admin')) return { title: 'Administration', description: 'Learnitive administration.', pathname, noIndex: true };
  if (pathname.startsWith('/share/')) return { title: 'Shared project', description: 'Open an authenticated Learnitive project share.', pathname, noIndex: true, robots: 'noindex,nofollow' };
  return { title: 'Page not found', description: 'The requested Learnitive page does not exist.', pathname, noIndex: true };
}

function SaasRoutes() {
  const pathname = usePathname();
  const { config } = useSaasSession();
  const searchParams = React.useMemo(() => new URLSearchParams(window.location.search), [pathname]);
  usePageMetadata(routeMetadata(pathname, searchParams));
  React.useEffect(() => {
    if (pathname === '/doc' || pathname.startsWith('/doc/')) {
      window.location.replace(pathname.replace(/^\/doc/u, '/docs') + window.location.search + window.location.hash);
      return;
    }
    if (pathname === '/examples' || pathname.startsWith('/examples/')) {
      window.location.replace('/templates' + window.location.search);
    }
  }, [pathname]);

  if (pathname === '/admin/login') return <AdminLoginPage />;
  if (pathname === '/terminal' || pathname === '/appterminal') return <TerminalStandalonePage />;
  if (pathname === '/admin' || pathname.startsWith('/admin/')) return <AdminArea pathname={pathname} />;
  if (pathname.startsWith('/join/')) return <ApplicationPage><JoinCollaborationPage /></ApplicationPage>;
  if (pathname.startsWith('/share/')) return <ApplicationPage><ShareProjectPage token={decodeURIComponent(pathname.slice('/share/'.length))} /></ApplicationPage>;
  if (pathname === '/projects') return <ApplicationPage footer><ProjectsPage /></ApplicationPage>;
  if (pathname === '/explore') return <ApplicationPage footer><ExploreProjectsPage /></ApplicationPage>;
  if (pathname === '/settings') return <ApplicationPage><SettingsPage /></ApplicationPage>;

  if (pathname.startsWith('/try/')) return <ApplicationPage><TryLearnExamplePage token={decodeURIComponent(pathname.slice('/try/'.length))} /></ApplicationPage>;
  if (pathname.startsWith('/learn/')) {
    const learnParts=pathname.slice('/learn/'.length).split('/').filter(Boolean).map(decodeURIComponent);
    if(learnParts.length===2&&learnParts[1]==='certificate') return <LearnLazyBoundary><LearnCertificatePage slug={learnParts[0]}/></LearnLazyBoundary>;
    if(learnParts.length>=2) return <LearnLazyBoundary><LearnPlayerPage courseSlug={learnParts[0]} lessonSlug={learnParts[1]}/></LearnLazyBoundary>;
  }

  let content: React.ReactNode;
  if (pathname === '/') content = <HomePage />;
  else if (pathname === '/features') content = <FeaturesPage />;
  else if (pathname === '/compare') content = <CompareIndexPageContent />;
  else if (pathname === '/compare/overleaf') content = <CompareDetailPageContent slug="overleaf" freeProjectLimit={config?.freePlanProjectLimit} />;
  else if (pathname.startsWith('/compare/')) content = <NotFoundPage />;
  else if (pathname === '/migrate') content = <NotFoundPage />;
  else if (pathname.startsWith('/migrate/')) content = <NotFoundPage />;
  else if (pathname === '/use-cases') content = <UseCasesPage />;
  else if (pathname === '/software-list') content = <NotFoundPage />;
  else if (pathname.startsWith('/use-cases/')) content = <UseCasePage slug={decodeURIComponent(pathname.slice('/use-cases/'.length))} />;
  else if (pathname === '/templates') content = <TemplatesPage />;
  else if (pathname === '/templates/institutions') content = <TemplatesInstitutionsPage />;
  else if (pathname.startsWith('/templates/institutions/')) content = <TemplateInstitutionPage slug={decodeURIComponent(pathname.slice('/templates/institutions/'.length))} />;
  else if (pathname.startsWith('/templates/categories/')) content = <TemplatesCategoryPage slug={decodeURIComponent(pathname.slice('/templates/categories/'.length))} />;
  else if (pathname.startsWith('/templates/')) content = <TemplateDetailPage slug={decodeURIComponent(pathname.slice('/templates/'.length))} />;
  else if (pathname === '/tools') content = <ToolsPage />;
  else if (pathname.startsWith('/tools/')) content = <ToolPage slug={decodeURIComponent(pathname.slice('/tools/'.length))} />;
  else if (pathname === '/pricing') content = <PricingPage />;
  else if (pathname === '/blog') content = <BlogPage />;
  else if (pathname.startsWith('/blog/')) content = <BlogDetailPage slug={decodeURIComponent(pathname.slice('/blog/'.length))} />;
  else if (pathname === '/change-log') content = <ChangeLogPage />;
  else if (pathname === '/docs') content = <DocsPage />;
  else if (pathname.startsWith('/docs/')) content = <DocDetailPage slug={decodeURIComponent(pathname.slice('/docs/'.length))} />;
  else if (pathname === '/learn') content = <LearnLazyBoundary><LearnCatalogPage /></LearnLazyBoundary>;
  else if (pathname.startsWith('/learn/')) content = <LearnLazyBoundary><LearnCoursePage slug={decodeURIComponent(pathname.slice('/learn/'.length)).replace(/^\/+|\/+$/gu,'')} /></LearnLazyBoundary>;
  else if (pathname === '/about') content = <AboutPage />;
  else if (pathname === '/faq') content = <FaqPage />;
  else if (pathname === '/apply') content = <ApplyLicensePage />;
  else if (pathname === '/contact') content = <ContactPage />;
  else if (pathname === '/privacy') content = <LegalPage slug="privacy" />;
  else if (pathname === '/terms') content = <LegalPage slug="terms" />;
  else if (pathname === '/login') content = <LoginPage register={false} />;
  else if (pathname === '/register') content = <LoginPage register />;
  else if (pathname === '/auth/magic') content = <MagicLoginPage />;
  else content = <NotFoundPage />;
  return <PublicPage>{content}</PublicPage>;
}

function TerminalStandalonePage() {
  return <main className="interactive-terminal-standalone" data-testid="standalone-terminal-page"><HeroCloudTerminal /></main>;
}

function TryLearnExamplePage({ token }: { token: string }) {
  const core = useCoreServices();
  const [state, setState] = React.useState<'creating' | 'error'>('creating');
  const [message, setMessage] = React.useState('Creating an isolated browser-local example project…');
  const started = React.useRef(false);
  React.useEffect(() => {
    if (started.current) return;
    started.current = true;
    const run = async () => {
      const payload = consumeLearnExample(token);
      if (!payload || Date.now() - payload.createdAt > 30 * 60 * 1000) {
        setState('error');
        setMessage('This learning example has expired or was already opened. Return to Learn and try it again.');
        return;
      }
      try {
        const baseName = `${payload.temporary ? 'Learn session' : 'Learn'} — ${payload.title}`.slice(0, 100);
        const name = await uniqueWorkspaceName(baseName, core.workspaceOps);
        const type: WorkspaceStorageType = supportsOpfsProjectStorage()
          ? WORKSPACE_STORAGE_TYPE.BrowserOPFS
          : WORKSPACE_STORAGE_TYPE.Browser;
        const mode = projectModeById(payload.projectModeId) ?? projectModeById('latex');
        if (!mode) throw new Error('This learning example uses an unsupported project mode.');
        const profile = canonicalProfileForMode(mode.id);
        const learnitiveConfig = mode.id === 'latex' ? createDefaultLearnitiveProjectConfig() : undefined;
        if (learnitiveConfig) learnitiveConfig.rootFile = payload.entryFile;
        if (payload.temporary) {
          const existing = await core.workspaceOps.getAllWorkspaces();
          for (const workspace of existing) {
            if (workspace.metadata?.learnTemporary !== true || workspace.type === WORKSPACE_STORAGE_TYPE.NativeFS) continue;
            try { await deleteBrowserProjectContents(core, workspace.name); } catch { /* best-effort ephemeral cleanup */ }
            await core.workspaceOps.deleteWorkspaceInfo(workspace.name).catch(() => undefined);
          }
        }
        await core.workspaceOps.createWorkspaceInfo({
          name,
          type,
          metadata: {
            ...(learnitiveConfig ? { project: learnitiveConfig } : {}),
            projectProfile: profile,
            projectModeId: mode.id,
            projectDisplayName: baseName,
            ...(payload.temporary ? { learnTemporary: true, learnSource: 'lesson', ...(payload.courseSlug?{learnCourseSlug:payload.courseSlug}:{}), ...(payload.lessonSlug?{learnLessonSlug:payload.lessonSlug}:{}) } : {}),
          },
        }, payload.temporary ? { bypassProjectLimit: true } : undefined);
        if (!payload.temporary) await requestPersistentBrowserStorage();
        const provider = await core.projectStorage.getProvider(name);
        const encoder = new TextEncoder();
        const baseStarter = PROJECT_STARTERS.find((starter) => starter.id === mode.starterId);
        const files: Record<string, string> = { ...(baseStarter?.files ?? {}), ...payload.files };
        for (const [path, contents] of Object.entries(files)) {
          await provider.writeFile(projectFilePath(path), encoder.encode(contents));
        }
        await finalizeCreatedBrowserProject(name, core);
        await core.workspaceState.refreshWorkspacesNow();
        try { window.history.replaceState(window.history.state, '', '/learn'); } catch { /* cosmetic cleanup only */ }
        window.location.href = core.navigation.toUri({ route: 'editor', payload: { wsPath: WsPath.fromParts(name, payload.entryFile).wsPath } });
      } catch (reason) {
        setState('error');
        setMessage(reason instanceof Error ? reason.message : String(reason));
      }
    };
    void run();
  }, [core, token]);
  return <section className="interactive-page-container interactive-page-heading interactive-try-example">
    <span className="interactive-eyebrow">Learn → Editor</span>
    <h1 className="interactive-display">{state === 'creating' ? 'Opening your learning workspace…' : 'Could not open this example'}</h1>
    <p>{message}</p>
    {state === 'error' ? <a className="interactive-text-link" href="/learn">Return to Learn →</a> : <LoaderCircle className="size-6 animate-spin" aria-label="Creating example project" />}
  </section>;
}

function JoinCollaborationPage() {
  const core = useCoreServices();
  const { auth, loading } = useSaasSession();
  const [shareUrl] = React.useState(() => window.location.href);
  const [runId, setRunId] = React.useState(0);
  const [progress, setProgress] = React.useState<{ message: string; completedFiles?: number; totalFiles?: number; completedBytes?: number; totalBytes?: number }>({ message: 'Connecting' });
  const [error, setError] = React.useState<string>();
  const started = React.useRef<number | undefined>(undefined);

  React.useEffect(() => {
    // The fragment contains the raw session secret. Keep the captured value in
    // memory for this join attempt, then remove it from the visible address bar.
    if (window.location.hash) window.history.replaceState(window.history.state, '', window.location.pathname);
  }, []);

  React.useEffect(() => {
    if (loading || started.current === runId) return;
    started.current = runId;
    let cancelled = false;
    setError(undefined);
    setProgress({ message: 'Connecting' });
    void core.projectCollaboration.joinNewProject(shareUrl, {
      ...(auth.authenticated ? { userName: auth.user.displayName, userId: String(auth.user.id), username: auth.user.email } : {}),
      requestAnonymousName: async (projectName) => {
        const result = await requestPagePrompt({
          title: `Join “${projectName}”`,
          description: 'Your name is shown beside your live Source cursor.',
          label: 'Your name',
          confirmText: 'Join',
        });
        return result?.trim() || undefined;
      },
      onProgress: (next) => { if (!cancelled) setProgress(next); },
    }).then(async (joined) => {
      if (cancelled) return;
      await core.workspaceState.refreshWorkspacesNow();
      // Joining a collaboration is invitation-gated, not a project-purchase
      // event. It must never consume one of a Free account's five owned-project
      // slots merely because the participant joined the room.
      toast.success(joined.reused ? `Reconnected to ${joined.wsName}.` : `Joined ${joined.wsName}.`);
      if (joined.rootFile) core.navigation.goWsFile(WsPath.fromParts(joined.wsName, joined.rootFile).wsPath);
      else core.navigation.goWorkspace(joined.wsName);
    }).catch((reason) => {
      if (!cancelled) setError(reason instanceof Error ? reason.message : String(reason));
    });
    return () => { cancelled = true; };
  }, [auth, core, loading, runId, shareUrl]);

  const percent = progress.totalBytes && progress.totalBytes > 0
    ? Math.min(100, Math.round(((progress.completedBytes ?? 0) / progress.totalBytes) * 100))
    : progress.totalFiles && progress.totalFiles > 0
      ? Math.min(100, Math.round(((progress.completedFiles ?? 0) / progress.totalFiles) * 100))
      : undefined;

  return <section className="interactive-empty-state" data-testid="interactive-collaboration-link-join">
    {error ? <>
      <h1 className="interactive-display">Unable to join project</h1>
      <p>{error}</p>
      <div className="flex flex-wrap justify-center gap-2"><Button onClick={() => setRunId((value) => value + 1)}>Retry</Button><a className="interactive-button interactive-button-secondary" href="/projects">Cancel</a></div>
    </> : <>
      <LoaderCircle className="interactive-project-join-spinner size-8" />
      <h1 className="interactive-display">Joining project…</h1>
      <p>{loading ? 'Preparing Learnitive' : progress.message}</p>
      {percent !== undefined ? <div className="interactive-project-join-progress" role="progressbar" aria-label="Shared project loading progress" aria-valuemin={0} aria-valuemax={100} aria-valuenow={percent}><span style={{ width: `${percent}%` }} /></div> : null}
      {progress.totalFiles ? <small>{progress.completedFiles ?? 0} of {progress.totalFiles} files{percent !== undefined ? ` · ${percent}%` : ''}</small> : <small>Verifying the live host before creating a browser project…</small>}
    </>}
  </section>;
}

function PublicPage({ children }: { children: React.ReactNode }) {
  return (
    <div className="interactive-site-shell">
      <PublicHeader />
      <main>{children}</main>
      <PublicFooter />
      <PublicSearch />
    </div>
  );
}

function ApplicationPage({ children, footer = false }: { children: React.ReactNode; footer?: boolean }) {
  return (
    <div className="interactive-site-shell interactive-app-page">
      <AppHeader />
      <main className="interactive-page-container interactive-app-main">{children}</main>
      {footer ? <PublicFooter /> : null}
      <PublicSearch />
    </div>
  );
}

const PUBLIC_PRIMARY_NAV = [
  ['Features', '/features'],
  ['Templates', '/templates'],
  ['Blog', '/blog'],
  ['Pricing', '/pricing'],
  ['About', '/about'],
] as const;

function Brand() {
  return (
    <a className="interactive-brand" href="/" aria-label="Learnitive home">
      <img className="interactive-brand-logo" src="/learnitive-logo-mobile.png" alt="" />
      <span className="interactive-brand-name">Learnitive</span>
    </a>
  );
}

function ThemeControl() {
  const { workbenchState } = useCoreServices();
  const preference = useAtomValue(workbenchState.$themePref);
  const next = preference === 'system' ? 'light' : preference === 'light' ? 'dark' : 'system';
  const label = `Theme: ${preference}. Switch to ${next}.`;
  const Icon = preference === 'dark' ? Moon : preference === 'light' ? Sun : Sparkles;
  return <button type="button" className="interactive-icon-button" aria-label={label} title={label} onClick={() => workbenchState.changeThemePreference(next)}><Icon className="size-4" /></button>;
}

function AccountControl() {
  const { auth, loading } = useSaasSession();
  if (loading) return <span className="interactive-avatar interactive-avatar-loading" aria-hidden />;
  return <LearnitiveAccountPopover auth={auth} />;
}

function PublicAccountControl() {
  const { auth, loading } = useSaasSession();
  if (loading) return <span className="interactive-button interactive-button-secondary interactive-header-login interactive-header-login-loading" aria-hidden>Login</span>;
  if (!auth.authenticated) return <a className="interactive-button interactive-button-secondary interactive-header-login" href="/login">Login</a>;
  return <LearnitiveAccountPopover auth={auth} />;
}

function PublicHeader() {
  const [mobile, setMobile] = React.useState(false);
  const pathname = usePathname();
  React.useEffect(() => { setMobile(false); }, [pathname]);
  const closeMobileMenu = () => setMobile(false);
  return (
    <header className="interactive-site-header">
      <div className="interactive-page-container interactive-header-inner">
        <Brand />
        <nav className="interactive-public-nav" aria-label="Primary">
          {PUBLIC_PRIMARY_NAV.map(([label, href]) => <a key={href} href={href}>{label}</a>)}
        </nav>
        <div className="interactive-header-actions">
          <SearchButton /><ThemeControl /><PublicAccountControl />
          <a className="interactive-button interactive-button-primary interactive-header-start" href="/projects">Open</a>
          <button type="button" className="interactive-icon-button interactive-mobile-menu-button" aria-label="Menu" onClick={() => setMobile((value) => !value)}>{mobile ? <X className="size-4" /> : <Menu className="size-4" />}</button>
        </div>
      </div>
      {mobile ? <nav className="interactive-mobile-nav" aria-label="Mobile">{PUBLIC_PRIMARY_NAV.map(([label, href]) => <a key={href} href={href} onClick={closeMobileMenu}>{label}</a>)}<a href="/projects" onClick={closeMobileMenu}>Open Workspace</a></nav> : null}
    </header>
  );
}

function AppHeader() {
  return <header className="interactive-app-header"><div className="interactive-page-container interactive-header-inner"><Brand /><nav className="interactive-app-nav" aria-label="Primary">{PUBLIC_PRIMARY_NAV.map(([label, href]) => <a key={href} href={href}>{label}</a>)}</nav><div className="interactive-header-actions"><SearchButton /><ThemeControl /><AccountControl /></div></div></header>;
}

function SearchButton() {
  return <button type="button" className="interactive-icon-button interactive-search-trigger" aria-label="Search" title="Search (/)" onClick={() => window.dispatchEvent(new CustomEvent('learnitive:public-search'))}><Search className="size-4" /></button>;
}

function PublicSearch() {
  const [open, setOpen] = React.useState(false);
  const [query, setQuery] = React.useState('');
  const [items, setItems] = React.useState<Array<{ title: string; href: string; type: string; searchText: string }>>([]);
  React.useEffect(() => {
    const shouldIgnore = (target: EventTarget | null) => {
      const element = target instanceof HTMLElement ? target : null;
      return Boolean(element?.closest('input,textarea,select,[contenteditable="true"],.cm-editor,.ProseMirror,math-field,[data-testid="project-terminal"]'));
    };
    const key = (event: KeyboardEvent) => {
      if (event.key === '/' && !event.metaKey && !event.ctrlKey && !event.altKey && !shouldIgnore(event.target)) {
        event.preventDefault(); setOpen(true);
      }
    };
    const custom = () => setOpen(true);
    window.addEventListener('keydown', key);
    window.addEventListener('learnitive:public-search', custom);
    return () => { window.removeEventListener('keydown', key); window.removeEventListener('learnitive:public-search', custom); };
  }, []);
  React.useEffect(() => {
    if (!open) return;
    const controller = new AbortController();
    const publicPages = [
      { title: 'Features', href: '/features', type: 'Page', searchText: 'Features Markdown visual writing WYSIWYG LaTeX PDF citations Paper Memory AI writer templates local first research privacy' },
      { title: 'Use Cases', href: '/use-cases', type: 'Page', searchText: 'Use Cases writing Markdown research thesis dissertation academic paper citations collaboration learning local first LaTeX' },
      { title: 'Learn', href: '/learn', type: 'Page', searchText: 'Learn courses tutorials writing Markdown LaTeX research citations documents study Free Pro' },
      { title: 'Free LaTeX Tools', href: '/tools', type: 'Page', searchText: 'BibTeX validator citation key checker LaTeX table generator error decoder free tools' },
      ...ALL_USE_CASES.map((item) => ({ title: item.title, href: `/use-cases/${item.slug}`, type: 'Use case', searchText: [item.title,item.summary,item.intro,...item.workflow].join(' ') })),
      { title: 'Pricing', href: '/pricing', type: 'Page', searchText: 'Pricing Free Pro Lifetime Monthly Deal LTD subscription license entitlement' },
      { title: 'About Learnitive', href: '/about', type: 'Page', searchText: 'About Learnitive local first writing Markdown WYSIWYG AI LaTeX research citations collaboration learning' },
      
      { title: 'Change log', href: '/change-log', type: 'Page', searchText: 'Change log changelog release notes updates recent changes new features fixes Learnitive' },
      { title: 'Apply Pro license', href: '/apply', type: 'Page', searchText: 'Apply Learnitive Pro license Lifetime Monthly Payhip license code' },
      { title: 'Contact', href: '/contact', type: 'Page', searchText: 'Contact support help ticket email' },
      { title: 'Privacy', href: '/privacy', type: 'Page', searchText: 'Privacy local files manuscript data account' },
      { title: 'Terms', href: '/terms', type: 'Page', searchText: 'Terms service license account' },
    ];
    void Promise.all([
      loadProjectTemplateManifest(controller.signal).then((m) => m.templates.map((t) => ({
        title: t.name,
        href: `/templates/${t.slug}`,
        type: 'Template',
        searchText: [t.name, t.description, t.category, t.author, ...t.tags].filter(Boolean).join(' '),
      }))).catch(() => []),
      listDocs().then((rows) => rows.map((item) => ({
        title: item.title,
        href: `/docs/${item.slug}`,
        type: 'Docs',
        searchText: [item.title, item.excerpt, item.category, ...(item.tags ?? [])].filter(Boolean).join(' '),
      }))).catch(() => []),
      listBlog().then((rows) => rows.map((item) => ({
        title: item.title,
        href: `/blog/${item.slug}`,
        type: 'Blog',
        searchText: [item.title, item.excerpt, item.author, ...(item.tags ?? [])].filter(Boolean).join(' '),
      }))).catch(() => []),
      listLearnCourses().then((rows) => rows.map((item) => ({
        title: item.title,
        href: `/learn/${item.slug}`,
        type: item.accessTier === 'pro' ? 'Pro course' : 'Course',
        searchText: [item.title, item.shortDescription, item.projectModeId, item.level, item.accessTier].filter(Boolean).join(' '),
      }))).catch(() => []),
    ]).then((groups) => { if (!controller.signal.aborted) setItems([...publicPages, ...groups.flat()]); });
    return () => controller.abort();
  }, [open]);
  const needle = query.trim().toLowerCase();
  const filtered = needle ? items.filter((item) => item.searchText.toLowerCase().includes(needle)).slice(0, 12) : items.slice(0, 8);
  return <Dialog open={open} onOpenChange={setOpen}><DialogContent className="sm:max-w-xl"><DialogHeader><DialogTitle>Search Learnitive</DialogTitle><DialogDescription>Search templates, courses, documentation, published articles and public Learnitive pages.</DialogDescription></DialogHeader><Input autoFocus value={query} onChange={(event) => setQuery(event.currentTarget.value)} placeholder="Search…" /><div className="interactive-search-results">{filtered.map((item) => <a key={`${item.type}-${item.href}`} href={item.href}><span>{item.title}</span><small>{item.type}</small></a>)}{filtered.length === 0 ? <p>No matching public content.</p> : null}</div></DialogContent></Dialog>;
}

function PublicFooter() {
  const groups = [
    ['Product', [['Projects','/projects'],['Learn','/learn'],['Academic Writer','/projects'],['Research','/features'],['Templates','/templates'],['Pricing','/pricing'],['Features','/features'],['Legacy','https://legacy.learnitive.com']]],
    ['Research & AI', [['AI Overview','/docs/ai-overview'],['Local AI','/docs/ai-overview'],['Ollama','/docs/ollama'],['AI Review','/docs/ai-review'],['Paper Memory','/docs/paper-memory'],['Citations','/features'],['Research','/features']]],
    ['Resources', [['Learn','/learn'],['Templates','/templates'],['Tools','/tools'],['Compare','/compare'],['Docs','/docs'],['Blog','/blog'],['Contact','/contact'],['Apply License','/apply']]],
    ['Company', [['About','/about'],['Change Log','/change-log'],['Privacy','/privacy'],['Terms','/terms']]],
  ] as const;
  return <footer className="interactive-site-footer"><div className="interactive-page-container interactive-footer-grid"><div className="interactive-footer-brand"><Brand /><p>Learn, research and write in a local-first workspace.</p></div>{groups.map(([title,links]) => <div key={title}><h2>{title}</h2>{links.map(([label,href]) => { const external = href.startsWith('http'); return <a key={`${title}-${href}-${label}`} href={href} {...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}>{label}</a>; })}</div>)}</div><div className="interactive-page-container interactive-footer-bottom"><span>© {new Date().getFullYear()} Learnitive</span><span>Your work stays where you choose.</span></div></footer>;
}

function HeroActions() {
  return <div className="interactive-hero-actions"><a className="interactive-button interactive-button-primary interactive-button-lg" href="/projects">Open Workspace <ArrowRight className="size-4" /></a><a className="interactive-button interactive-button-secondary interactive-button-lg" href="/templates">Browse Templates</a></div>;
}

function HomePage() {
  const { auth, config, loading } = useSaasSession();
  const proAccess = auth.authenticated && (auth.entitlement.pro || auth.user.role === 'admin');
  return <>
    <HomeHero lifetimeShopUrl={config?.shopUrl} />
    <HomeAiBuilder authenticated={auth.authenticated} authLoading={loading} proAccess={proAccess} settings={config?.homeAiBuilder} />
    <HomeWorkflowsShowcase />
    <HomeProductComparison />
    <HomeCommunityReviews />
    <div data-home-section="pricing"><LearnitivePricingTable variant="home" lifetimeShopUrl={config?.shopUrl} monthlyShopUrl={config?.monthlyShopUrl} freeProjectLimit={config?.freePlanProjectLimit} /></div>
    <div data-home-section="faq"><PublicFaq title="Questions about Learnitive" items={HOME_FAQ} freeProjectLimit={config?.freePlanProjectLimit} /></div>
    <div data-home-section="final-cta"><PublicFinalCta heading="Ready to learn, research and write with Learnitive?" copy="Start a project, continue learning or choose a writing and research template." primaryLabel="Open Workspace" primaryHref="/projects" secondaryLabel="Browse Templates" secondaryHref="/templates" /></div>
  </>;
}

function FeaturesPage() {
  return <FeaturesPageContent />;
}

type ResearchUseCase = {
  slug: string;
  eyebrow: string;
  title: string;
  summary: string;
  goal: string;
  challenge: string;
  intro: string;
  capabilities: readonly [string, string][];
  workflow: readonly string[];
};

const RESEARCH_USE_CASES: readonly ResearchUseCase[] = [
  { slug:'researchers', eyebrow:'Researchers', title:'Research', summary:'Write the manuscript, manage references and keep research context beside the LaTeX project.', goal:'Produce a research paper without splitting the manuscript, references, evidence and analysis across disconnected tools.', challenge:'Research writing mixes equations, figures, citations, revisions and source material. Keeping those pieces traceable becomes harder as the project grows.', intro:'Learnitive keeps the manuscript as real LaTeX while putting research-specific tools around it. Work in Visual or Source mode, compile locally and keep project assets close to the paper.', capabilities:[['Write the paper','Use Visual or Source editing for text, equations, figures and citations.'],['Work with evidence','Use Paper Memory and reference workflows when research questions need source-grounded context.'],['Keep it portable','Keep project files, figures and compilation outputs beside the manuscript.'],['Review the result','Inspect PDF, logs and SyncTeX without leaving the workspace.']], workflow:['Create or import the project','Write and cite','Check evidence and review changes','Compile the PDF','Export or archive the project'] },
  { slug:'thesis-dissertation', eyebrow:'Thesis writers', title:'Thesis Writing', summary:'Organize chapters, references, figures and institution templates around one maintainable LaTeX project.', goal:'Produce a long-form thesis or dissertation that remains understandable months into the writing process.', challenge:'Long documents accumulate chapters, figures, bibliography files, institution requirements and supervisor feedback. A stable project structure matters as much as the editor.', intro:'Learnitive supports multi-file LaTeX projects, project-root selection, bibliography workflows and thesis templates while leaving the underlying source directly accessible.', capabilities:[['Structure chapters','Keep chapters in focused files under one root document.'],['Manage references','Keep bibliography and citations inside the project.'],['Start from a template','Use institution and thesis templates where verified catalog metadata is available.'],['Review revisions','Use comments and review tools while keeping the source portable.']], workflow:['Choose or import a thesis template','Organize chapters and assets','Write and manage references','Review revisions','Compile and export'] },
  { slug:'research-teams', eyebrow:'Research teams', title:'Research Teams', summary:'Share a live project for Source editing, comments and review while each participant keeps a normal Learnitive project.', goal:'Move a paper through co-author or supervisor review without turning the manuscript into a proprietary document format.', challenge:'Collaborative LaTeX can become fragile when cursor state, review discussion and source changes are handled by separate systems.', intro:'Learnitive live collaboration focuses on Source editing with collaborator presence, shared project links and review workflows around the same project files.', capabilities:[['Collaborate in Source','Edit shared LaTeX text with named collaborator presence.'],['Share simply','Invite collaborators with a project link or QR code.'],['Review changes','Keep comments and review activity close to the manuscript.'],['Compile locally','Participants can continue using the browser compilation workflow.']], workflow:['Start a collaboration session','Share the project link','Edit and review Source together','Resolve comments and changes','Compile and continue locally'] },
  { slug:'local-first-latex', eyebrow:'Project ownership', title:'Local First LaTeX', summary:'Work from Browser storage, optionally synchronize every project to one Projects Folder, compile locally and choose explicitly when a cloud-backed capability receives project data.', goal:'Write LaTeX in a modern web interface while keeping ownership of the project files.', challenge:'Many web editors make cloud storage inseparable from writing. That can be inconvenient for local folders, offline work and reproducible project workflows.', intro:'Every normal Learnitive project works from Browser storage. You can optionally connect one Projects Folder that synchronizes all projects without turning the manuscript into proprietary cloud storage or requiring an account.', capabilities:[['Browser storage','Start without installing a desktop TeX distribution.'],['Projects Folder','Synchronize all projects to one folder on supported browsers without an account.'],['Local compilation','Compile supported projects on the device.'],['Portable source','Export standard project files whenever you choose.']], workflow:['Create the Browser project','Optionally connect one Projects Folder','Edit ordinary project files','Compile on the device','Use online features only when useful','Export or keep working locally'] },
  { slug:'visual-latex-editor', eyebrow:'Editing', title:'Visual LaTeX', summary:'Move between manuscript-focused Visual editing and direct LaTeX Source editing while keeping one underlying project.', goal:'Make everyday manuscript editing easier without hiding or replacing the LaTeX source.', challenge:'Visual editors can become limiting when a document uses advanced packages or custom constructs, while source-only editors can slow down routine writing.', intro:'Learnitive uses Visual mode for supported manuscript structures and Source mode for direct control. Unsupported constructs remain represented rather than being silently discarded.', capabilities:[['Two editing modes','Switch between Visual and Source around the same project.'],['Math editing','Use MathLive for supported equations.'],['Tables and lists','Edit supported document structures visually.'],['Source Safe Fallback','Return to Source for advanced or unsupported constructs.']], workflow:['Open a LaTeX file','Choose Visual or Source','Edit text, math and tables','Use Source for advanced constructs','Compile and inspect PDF'] },
  { slug:'paper-memory', eyebrow:'Research context', title:'Paper Memory', summary:'Ask questions across papers, inspect supporting evidence and keep research context close to the manuscript.', goal:'Find and reuse evidence from a paper collection while writing without losing provenance.', challenge:'Searching PDFs manually becomes slow once a literature collection grows, and generic AI answers are not a substitute for inspectable paper evidence.', intro:'Paper Memory is available on Free and Pro for working with papers you add, retrieving relevant passages and moving from evidence toward citations in the manuscript.', capabilities:[['Paper library','Keep a research collection associated with the browser workflow.'],['Grounded retrieval','Find relevant passages for a research question.'],['Page provenance','Inspect where supporting evidence came from.'],['Citation workflow','Carry evidence into a stable citation and manuscript workflow.']], workflow:['Add papers','Index the collection','Ask a research question','Inspect evidence','Use or verify citations in the manuscript'] },
  { slug:'local-ai-latex', eyebrow:'Optional AI', title:'Local AI and LaTeX', summary:'Use browser Local AI, connect local models through Ollama, or choose BYOK AI on any plan when you need a cloud model.', goal:'Use AI around a LaTeX project without making a cloud model mandatory for the writing workflow.', challenge:'Different research tasks have different privacy, model-quality and cost requirements. A single mandatory provider is a poor fit for every manuscript.', intro:'Learnitive separates browser Local AI, local Ollama services and BYOK AI providers while keeping AI actions bounded around the editor and research tools.', capabilities:[['Local AI','Use supported local models where the browser can run them.'],['Ollama','Use models served locally by Ollama on this computer without an API key.'],['BYOK AI','Connect a supported provider key on any plan when a cloud model is useful.'],['LaTeX assistance','Use AI around concrete writing and compiler tasks.'],['Research grounding','Use Paper Memory when answers should come from papers you control.']], workflow:['Choose Local AI, Ollama or BYOK AI','Ask for a concrete task','Review the result','Apply edits deliberately','Keep the project independent of the model provider'] },
  { slug:'overleaf-alternative', eyebrow:'Making the switch', title:'Overleaf Alternative', summary:'Keep familiar LaTeX editing and collaboration while adding local projects, browser compilation and research tooling around files you control.', goal:'Move an existing LaTeX workflow into an editor that gives local project storage and browser-side tools a first-class role.', challenge:'Cloud collaboration is useful, but some researchers also need ordinary local files, local compilation, local AI, terminal workflows or projects that remain easy to use outside the editor.', intro:'Overleaf is a mature collaborative LaTeX platform. Learnitive takes a different architectural approach: local project options and browser-local compilation are core, with collaboration and optional cloud capabilities added when useful.', capabilities:[['Import existing work','Export an Overleaf project as ZIP and use Import ZIP in Learnitive.'],['Keep projects local','Use Browser storage and optionally synchronize all projects to one Projects Folder.'],['Compile in browser','Run the local compiler without a cloud compile queue.'],['Add research tooling','Use Paper Memory, scientific code and terminal workflows around the same project.']], workflow:['Export the existing project as ZIP','Choose Import ZIP in Projects','Write in Visual or Source','Compile locally','Use collaboration or research tools as needed'] },
] as const;

const DEVELOPMENT_USE_CASES: readonly ResearchUseCase[] = [
  { slug:'software-development', eyebrow:'Software development', title:'Software Development', summary:'Edit multi-file projects, use Git, run real Linux tools, build, test and preview from the browser.', goal:'Move through the complete development loop without stitching together a browser editor, remote shell and separate preview environment.', challenge:'Real projects need more than a text editor: dependencies, compilers, Git, terminals, logs and previews all need to stay connected to the same files.', intro:'Learnitive combines the Interactive IDE with a real Ubuntu environment and project-aware tooling so editing, running and debugging happen around one project.', capabilities:[['Interactive IDE','Edit multi-file JavaScript, TypeScript, Python, C, C++ and other projects with language-aware modes.'],['Linux runtime','Open multiple terminal sessions against the same authenticated Ubuntu environment.'],['Git workflows','Keep repositories, branches and command-line Git beside the editor.'],['Build and preview','Run, build, test and open private web previews from the same workspace.']], workflow:['Create, import or clone a project','Edit project files','Run commands or builds','Inspect output and preview','Commit, share or publish'] },
  { slug:'ai-assisted-development', eyebrow:'AI assisted development', title:'AI Assisted Coding', summary:'Use AI assistance, project-aware agents, local models or BYOK cloud AI without giving up the underlying project workflow.', goal:'Use AI where it saves time while keeping source, terminal output and project decisions inspectable.', challenge:'AI tools are most useful when they understand the project but do not become the only way to edit, run or recover it.', intro:'Interactive AI can assist with focused questions and edits, AI Agents can work across project tasks, and local Ollama or BYOK cloud models let you choose the model path that fits the job.', capabilities:[['Interactive AI','Ask focused coding, debugging and explanation questions.'],['AI Agents','Delegate multi-step project work that spans files and commands.'],['Local AI / Ollama','Use supported local models when you want a local model path.'],['BYOK cloud AI','Connect supported cloud providers with your own key when you choose.']], workflow:['Open the project','Choose assistant, agent or builder','Describe a bounded task','Review generated changes','Run and verify the result'] },
  { slug:'ai-app-building', eyebrow:'AI Builder', title:'AI App Building', summary:'Start with a prompt, let AI Builder create a project, then continue in a full IDE and Linux runtime.', goal:'Get from an idea to an editable working project quickly without ending in a closed prompt-only workflow.', challenge:'Prompt-generated apps are easy to start but can become difficult to maintain when there is no complete editor, terminal or project workflow behind them.', intro:'AI Builder creates a starting project inside Learnitive. From there the same project can be edited manually, continued with AI Agents, run in the terminal, previewed and published.', capabilities:[['Prompt to project','Describe what you want to build and choose a starting point.'],['Full IDE afterward','Inspect and change every generated source file.'],['Real runtime','Install project dependencies and run normal development commands.'],['Continue with agents','Use AI Agents for follow-up work rather than regenerating from scratch.']], workflow:['Describe the idea','Generate the project','Inspect it in Interactive IDE','Run and preview','Refine with code or AI Agents'] },
  { slug:'devops-linux', eyebrow:'DevOps and Linux', title:'Linux and DevOps', summary:'Use Ubuntu, Bash, Git, compilers and developer command-line tools without replacing your local project as the source of truth.', goal:'Practice and perform practical Linux development workflows from a browser-accessible environment.', challenge:'CLI and DevOps work needs a real process environment, not a simulated console, while disposable environments still need clear lifecycle controls.', intro:'Authenticated Learnitive users receive one plan-limited Ubuntu environment that can host multiple terminal sessions. Guest environments are disposable and automatically cleaned after their browser lease ends.', capabilities:[['Ubuntu shell','Work with Bash and standard development utilities.'],['Build toolchains','Use GCC, G++, Clang, CMake, Make and other certified tools.'],['Multiple PTYs','Open several terminal sessions against one authenticated environment.'],['Clear lifecycle','Close PTYs without destroying the environment, or explicitly destroy/reset it when needed.']], workflow:['Open the terminal','Inspect or sync the project','Run Linux commands','Build and test','Close the session or explicitly destroy the environment'] },
  { slug:'web-development', eyebrow:'Web development', title:'Web Development', summary:'Develop HTML, CSS, JavaScript, React, Vite and Node-based projects with a browser editor, terminal and private preview.', goal:'Build web projects without switching between an editor, local terminal and unrelated preview service.', challenge:'Frontend and full-stack prototyping becomes slower when source editing, package commands and preview URLs are disconnected.', intro:'Learnitive keeps the source project in the IDE while the Linux environment handles normal development commands and the preview pane exposes project output privately.', capabilities:[['Web templates','Start from validated HTML, React and framework templates.'],['Node toolchain','Use Node.js and project package managers in the runtime.'],['Live/private preview','Open project web output without publishing it publicly.'],['Publish when ready','Publish supported web projects only when you choose.']], workflow:['Start or import a web project','Install project dependencies','Edit and run the dev server','Inspect the private preview','Commit or publish'] },
  { slug:'scientific-computing', eyebrow:'Scientific computing', title:'Scientific Computing', summary:'Use Python, NumPy, SciPy, pandas, Matplotlib, SymPy, R and native compilers beside documents and project files.', goal:'Keep analysis, scripts, figures and technical documents together in a reproducible project.', challenge:'Scientific work often spans interpreted languages, compiled code, plots, data and documents that traditional web IDEs treat as separate workflows.', intro:'The certified Learnitive development image includes a scientific Python stack plus R and native toolchains, while LaTeX-PDF workflows can live in the same project.', capabilities:[['Scientific Python','Use NumPy, SciPy, pandas, Matplotlib and SymPy from the certified runtime.'],['R workflows','Run R scripts beside project data and documents.'],['C and C++','Compile numerical or systems code with GCC/G++.'],['Research documents','Generate figures or data and reference them from LaTeX projects.']], workflow:['Create or import the project','Run analysis or simulation','Generate figures and outputs','Compile or preview results','Version the reproducible project'] },
  { slug:'education-learning', eyebrow:'Education and learning', title:'Learning', summary:'Combine interactive lessons, project templates, validators, code and terminal work so learning happens inside the tools being taught.', goal:'Move learners from reading instructions to actually building and running projects.', challenge:'Static tutorials can explain syntax without teaching the environment, debugging and workflow skills needed to finish a real project.', intro:'Interactive Learn can pair lessons with temporary project workspaces, code examples, validators and the same editor/runtime model used for normal projects.', capabilities:[['Interactive lessons','Follow guided programming, web, Markdown and LaTeX material.'],['Real project context','Practice inside the same editor and runtime model used outside Learn.'],['Validators and quizzes','Check progress with lesson-specific verification.'],['Templates','Start exercises from known working project structures.']], workflow:['Choose a learning path','Open the lesson workspace','Change code or documents','Run and validate','Continue with a real project'] },
  { slug:'collaboration-teams', eyebrow:'Collaboration', title:'Collaboration', summary:'Share authenticated project access, collaborate on source and review work without turning collaboration into the only copy of the project.', goal:'Let teams, supervisors and collaborators work around the same project while preserving explicit project ownership and access.', challenge:'Collaboration is useful only when permissions, previews and source ownership remain understandable.', intro:'Learnitive requires authenticated collaboration participants. Starting a collaboration requires Pro, while invited collaborators join through authenticated project access.', capabilities:[['Authenticated access','All collaboration participants sign in before accessing the shared project.'],['Source collaboration','Work around the same source project with presence and review tools.'],['Private previews','Keep development previews private to authorized project access.'],['Portable project','Continue to use ZIP, Git and local-first workflows outside the collaboration session.']], workflow:['Open the project','Start collaboration','Invite authenticated collaborators','Edit and review together','Close collaboration and keep the project'] },
  { slug:'local-first-projects', eyebrow:'Local first projects', title:'Local First Projects', summary:'Start every project in Browser storage, optionally synchronize all projects to one Projects Folder, and use ZIP, Git or cloud runtimes when they help.', goal:'Use a browser IDE without making permanent cloud project storage the unavoidable source of truth.', challenge:'Fully hosted IDEs can make moving a project in or out feel like migration instead of a normal file workflow.', intro:'Learnitive treats ordinary project files as the unit of work. Browser storage is the working copy, an optional Projects Folder is the durable filesystem replica, and temporary Linux environments can sync changes back into the project.', capabilities:[['Projects Folder','Connect one folder without an account and synchronize all projects as ordinary files.'],['ZIP import/export','Move complete projects in and out without a proprietary format.'],['Git repositories','Use Git as a durable project workflow when appropriate.'],['Temporary runtime','Use the Ubuntu environment for execution without making the container the permanent project store.']], workflow:['Create or import a Browser project','Optionally connect one Projects Folder','Edit the project','Sync to runtime when needed','Run and sync changes back','Keep or export the project'] },
] as const;

const ALL_USE_CASES: readonly ResearchUseCase[] = RESEARCH_USE_CASES;

function UseCasesPage() {
  return <>
    <MarketingPageHeading eyebrow="Use Cases" title="Writing and Research Workflows" copy="Use Learnitive for Markdown writing, academic LaTeX, thesis projects, citations, research context, collaboration and local-first document workflows." />
    <section className="interactive-page-container interactive-use-case-section" aria-labelledby="research-use-cases-title">
      <div className="interactive-section-heading interactive-centered-heading"><span className="interactive-eyebrow">Write & research</span><h2 id="research-use-cases-title">Documents that stay portable</h2><p>Choose the workflow closest to your document, research or learning goal.</p></div>
      <div className="interactive-use-case-index">{RESEARCH_USE_CASES.map((item) => <a className="interactive-use-case-index-card" key={item.slug} href={`/use-cases/${item.slug}`}><span className="interactive-eyebrow">{item.eyebrow}</span><h3>{item.title}</h3><p>{item.summary}</p><span className="interactive-text-link">Read use case <ChevronRight className="size-4" aria-hidden="true" /></span></a>)}</div>
    </section>
    <section className="interactive-page-container interactive-use-case-workflow-map" aria-labelledby="workflow-map-title"><div className="interactive-section-heading"><span className="interactive-eyebrow">Start</span><h2 id="workflow-map-title">Choose a Workflow</h2></div><div className="interactive-use-case-workflow-grid">{[
      ['Write an article or notes','Markdown Writer'],['Draft a thesis or paper','LaTeX Writer'],['Work with citations','Bibliography and research tools'],['Use AI for a draft','AI Writer'],['Use AI locally','Local AI'],['Bring your own model key','BYOK'],['Keep files local','Projects Folder'],['Collaborate on a document','Shared project'],['Learn a writing workflow','Learnitive Learn'],
    ].map(([goal,start]) => <div key={goal}><span>{goal}</span><strong>{start}</strong></div>)}</div></section>
    <PublicFinalCta heading="Start Writing" copy="Start a Markdown or LaTeX project, or begin from a writing template." primaryLabel="Open Projects" primaryHref="/projects" secondaryLabel="Browse Templates" secondaryHref="/templates" />
  </>;
}

function UseCasePage({ slug }: { slug: string }) {
  const item = ALL_USE_CASES.find((entry) => entry.slug === slug);
  if (!item) return <NotFoundPage />;
  return <article className="interactive-page-container interactive-use-case-detail interactive-use-case-detail-v2">
    <header className="interactive-use-case-hero"><a className="interactive-text-link" href="/use-cases">← Use Cases</a><span className="interactive-eyebrow">{item.eyebrow}</span><h1>{item.title}</h1><p>{item.summary}</p></header>
    <section className="interactive-use-case-context"><div><span className="interactive-eyebrow">The goal</span><h2>Goal</h2><p>{item.goal}</p></div><div><span className="interactive-eyebrow">The friction</span><h2>Challenge</h2><p>{item.challenge}</p></div></section>
    <section className="interactive-use-case-help"><div><span className="interactive-eyebrow">How Learnitive helps</span><h2>Workflow</h2><p>{item.intro}</p></div><div className="interactive-use-case-capabilities">{item.capabilities.map(([title, copy]) => <div key={title}><Check className="size-4" aria-hidden="true" /><div><h3>{title}</h3><p>{copy}</p></div></div>)}</div></section>
    <section className="interactive-use-case-next"><div className="interactive-section-heading"><span className="interactive-eyebrow">Workflow</span><h2>Next Steps</h2></div><ol>{item.workflow.map((step, index) => <li key={step}><span>{String(index + 1).padStart(2, '0')}</span><strong>{step}</strong></li>)}</ol></section>
    <PublicFinalCta heading="Continue in Learnitive" copy="Open your project or start from a template." />
  </article>;
}

function MarketingPageHeading({ eyebrow, title, copy }: { eyebrow: string; title: string; copy: string }) {
  return <header className="interactive-page-container interactive-page-heading"><span className="interactive-eyebrow">{eyebrow}</span><h1 className="interactive-display">{title}</h1>{copy ? <p>{copy}</p> : null}</header>;
}

function useTemplates() {
  const [templates, setTemplates] = React.useState<readonly ProjectTemplateDefinition[]>([]);
  const [institutions, setInstitutions] = React.useState<readonly ProjectTemplateInstitution[]>([]);
  const [categories, setCategories] = React.useState<readonly ProjectTemplateCategory[]>([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState('');
  React.useEffect(() => {
    const controller = new AbortController();
    void Promise.all([
      loadProjectTemplateManifest(controller.signal),
      loadProjectTemplateInstitutions(controller.signal).catch(() => [] as readonly ProjectTemplateInstitution[]),
      loadProjectTemplateCategories(controller.signal).catch(() => [] as readonly ProjectTemplateCategory[]),
    ]).then(([manifest, institutionRows, categoryRows]) => {
      setTemplates(manifest.templates);
      setInstitutions(institutionRows);
      setCategories(categoryRows);
    }).catch((reason) => setError(reason instanceof Error ? reason.message : String(reason))).finally(() => {
      if (!controller.signal.aborted) setLoading(false);
    });
    return () => controller.abort();
  }, []);
  return { templates, institutions, categories, loading, error };
}

function LoadingCard() { return <div className="interactive-loading-card interactive-loading-wireframe" role="status"><span className="sr-only">Loading</span><i aria-hidden="true"/><i aria-hidden="true"/><i aria-hidden="true"/></div>; }

function templateInstitutionName(template: ProjectTemplateDefinition, institutions: readonly ProjectTemplateInstitution[]) {
  if (!template.institution) return '';
  const institution = institutions.find((item) => item.id === template.institution);
  return institution?.displayName || institution?.name || template.institution;
}

function InstitutionMark({ institution }: { institution: ProjectTemplateInstitution }) {
  return <span className="interactive-template-institution-mark">{institution.logoUrl ? <img src={institution.logoUrl} alt="" loading="lazy" /> : institution.shortName || institution.displayName.slice(0, 3)}</span>;
}

const TEMPLATE_PAGE_SIZE = 12;

function templateCategorySlug(value: string) {
  return value.normalize('NFKD').replace(/\p{Diacritic}/gu, '').toLowerCase().replaceAll('&', ' and ').replace(/[^a-z0-9]+/gu, '-').replace(/^-+|-+$/gu, '');
}

function templateTimestamp(template: ProjectTemplateDefinition) {
  const value = template.lastVerified || template.updatedAt || '';
  const time = Date.parse(value);
  return Number.isFinite(time) ? time : 0;
}

function templateSearchText(template: ProjectTemplateDefinition, institutions: readonly ProjectTemplateInstitution[]) {
  const institution = template.institution ? institutions.find((item) => item.id === template.institution) : undefined;
  return [
    template.name, template.description, template.primaryLanguage, template.framework, template.useCaseId, template.category, institution?.name, institution?.displayName,
    institution?.shortName, ...(institution?.aliases || []), template.country, template.region, ...template.degreeLevels, ...template.tags,
    ...template.features, template.compiler, template.documentClass, template.journal, template.publisher,
    template.conference,
  ].filter(Boolean).join(' ').toLowerCase();
}

function TemplatesPage() {
  const { templates, institutions, categories, loading, error } = useTemplates();
  const launchTemplates = React.useMemo(() => templates.filter((template) => template.projectModeId === 'markdown' || template.projectModeId === 'latex'), [templates]);
  const seoIndexableLaunchTemplates = React.useMemo(
    () => launchTemplates.filter((template) => template.status === 'published' && template.validationStatus === 'valid' && !template.noIndex),
    [launchTemplates],
  );
  const initialFilters = React.useMemo(() => new URLSearchParams(window.location.search), []);
  const [query, setQuery] = React.useState(() => initialFilters.get('search') || '');
  const [tier, setTier] = React.useState<'all'|'free'|'pro'>(() => {
    const value = initialFilters.get('tier');
    return value === 'free' || value === 'pro' ? value : 'all';
  });
  const [language, setLanguage] = React.useState(() => initialFilters.get('language') || 'all');
  const [framework, setFramework] = React.useState(() => initialFilters.get('framework') || 'all');
  const [useCase, setUseCase] = React.useState(() => initialFilters.get('useCase') || 'all');
  const [category, setCategory] = React.useState(() => initialFilters.get('category') || 'all');
  const [institution, setInstitution] = React.useState(() => initialFilters.get('institution') || 'all');
  const [country, setCountry] = React.useState(() => initialFilters.get('country') || 'all');
  const [degree, setDegree] = React.useState(() => initialFilters.get('degree') || 'all');
  const [compiler, setCompiler] = React.useState(() => initialFilters.get('compiler') || 'all');
  const [sort, setSort] = React.useState<'verified'|'updated'|'az'>('verified');
  const [page, setPage] = React.useState(() => { const value = Number(initialFilters.get('page') || 1); return Number.isSafeInteger(value) && value > 0 ? value : 1; });
  const [quickPreview, setQuickPreview] = React.useState<ProjectTemplateDefinition | null>(null);

  React.useEffect(() => {
    if (category === 'all' || !categories.length || categories.some((item) => item.id === category)) return;
    const legacyAliases: Record<string, string> = { journal: 'journal-articles', conference: 'conference-papers', thesis: 'theses-and-dissertations' };
    const normalized = templateCategorySlug(category);
    const resolved = categories.find((item) => item.id === legacyAliases[normalized] || item.slug === normalized || templateCategorySlug(item.name) === normalized);
    if (resolved) setCategory(resolved.id);
  }, [categories, category]);

  React.useEffect(() => {
    const params = new URLSearchParams();
    if (query.trim()) params.set('search', query.trim());
    if (tier !== 'all') params.set('tier', tier);
    if (language !== 'all') params.set('language', language);
    if (framework !== 'all') params.set('framework', framework);
    if (useCase !== 'all') params.set('useCase', useCase);
    if (category !== 'all') params.set('category', category);
    if (institution !== 'all') params.set('institution', institution);
    if (country !== 'all') params.set('country', country);
    if (degree !== 'all') params.set('degree', degree);
    if (compiler !== 'all') params.set('compiler', compiler);
    if (page > 1) params.set('page', String(page));
    const next = `${window.location.pathname}${params.size ? `?${params.toString()}` : ''}`;
    window.history.replaceState(window.history.state, '', next);
    const hasIndexableInventory = !loading && seoIndexableLaunchTemplates.length > 0;
    const robots = params.size || !hasIndexableInventory
      ? 'noindex,follow,max-image-preview:large'
      : 'index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1';
    upsertMeta('meta[name="robots"]', { name: 'robots', content: robots });
    const canonical = new URL('/templates', `${PRODUCT_IDENTITY.origin}/`).toString();
    let link = document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]');
    if (!link) { link = document.createElement('link'); link.rel = 'canonical'; document.head.appendChild(link); }
    link.href = canonical;
  }, [category, compiler, country, degree, framework, institution, language, loading, page, query, seoIndexableLaunchTemplates.length, tier, useCase]);

  const categoryCounts = new Map<string, number>();
  const institutionCounts = new Map<string, number>();
  for (const template of launchTemplates) {
    if (template.categoryId) categoryCounts.set(template.categoryId, (categoryCounts.get(template.categoryId) || 0) + 1);
    if (template.institution) institutionCounts.set(template.institution, (institutionCounts.get(template.institution) || 0) + 1);
  }
  const browseCategories = categories.filter((item) => (categoryCounts.get(item.id) || 0) > 0);
  const countries = [...new Set(launchTemplates.map((template) => template.country).filter((value): value is string => Boolean(value)))].sort();
  const degrees = [...new Set(launchTemplates.flatMap((template) => [...template.degreeLevels]))].sort();
  const compilers = [...new Set(launchTemplates.map((template) => template.compiler).filter((value): value is NonNullable<ProjectTemplateDefinition['compiler']> => Boolean(value)))].sort();
  const languages = [...new Set(launchTemplates.map((template) => template.primaryLanguage).filter(Boolean))].sort();
  const frameworks = [...new Set(launchTemplates.filter((template) => language === 'all' || template.primaryLanguage === language).map((template) => template.framework).filter((value): value is string => Boolean(value)))].sort();
  const useCases = [...new Set(launchTemplates.map((template) => template.useCaseId).filter(Boolean))].sort();
  React.useEffect(() => { if (framework !== 'all' && !frameworks.includes(framework)) setFramework('all'); }, [framework, frameworks.join('|')]);
  const recentlyVerified = [...launchTemplates].sort((a, b) => templateTimestamp(b) - templateTimestamp(a) || a.name.localeCompare(b.name)).slice(0, 8);
  const freshModeOrder = ['markdown','latex'];
  const freshStartingPoints = [...launchTemplates]
    .filter((template) => template.useCaseId === 'starter')
    .sort((a, b) => {
      const ai = freshModeOrder.indexOf(a.projectModeId);
      const bi = freshModeOrder.indexOf(b.projectModeId);
      return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi) || templateTimestamp(b) - templateTimestamp(a) || a.name.localeCompare(b.name);
    })
    .filter((template, index, rows) => rows.findIndex((candidate) => candidate.projectModeId === template.projectModeId) === index)
    .slice(0, 8);
  const displayedFreshStartingPoints = freshStartingPoints.length ? freshStartingPoints : recentlyVerified;
  const needle = query.trim().toLowerCase();
  const filtered = launchTemplates.filter((template) => (tier === 'all' || template.tier === tier)
      && (language === 'all' || template.primaryLanguage === language)
      && (framework === 'all' || template.framework === framework)
      && (useCase === 'all' || template.useCaseId === useCase)
      && (category === 'all' || template.categoryId === category || template.category === category)
      && (institution === 'all' || template.institution === institution)
      && (country === 'all' || template.country === country)
      && (degree === 'all' || template.degreeLevels.includes(degree))
      && (compiler === 'all' || template.compiler === compiler)
      && (!needle || templateSearchText(template, institutions).includes(needle)));
  const sorted = [...filtered].sort((a, b) => {
    if (sort === 'az') return a.name.localeCompare(b.name);
    if (sort === 'updated') return Date.parse(b.updatedAt || '') - Date.parse(a.updatedAt || '') || a.name.localeCompare(b.name);
    return templateTimestamp(b) - templateTimestamp(a) || a.name.localeCompare(b.name);
  });
  const totalPages = Math.max(1, Math.ceil(sorted.length / TEMPLATE_PAGE_SIZE));
  const filterKey = JSON.stringify([query, tier, language, framework, useCase, category, institution, country, degree, compiler, sort]);
  const previousFilterKey = React.useRef(filterKey);
  React.useEffect(() => {
    if (previousFilterKey.current === filterKey) return;
    previousFilterKey.current = filterKey;
    setPage(1);
  }, [filterKey]);
  React.useEffect(() => { if (!loading && page > totalPages) setPage(totalPages); }, [loading, page, totalPages]);
  const currentPage = Math.min(page, totalPages);
  const pageStart = (currentPage - 1) * TEMPLATE_PAGE_SIZE;
  const pageRows = sorted.slice(pageStart, pageStart + TEMPLATE_PAGE_SIZE);
  const goToTemplatePage = (nextPage: number) => {
    setPage(Math.min(totalPages, Math.max(1, nextPage)));
    window.requestAnimationFrame(() => document.getElementById('all-templates')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
  };
  const clearFilters = () => { setQuery(''); setTier('all'); setLanguage('all'); setFramework('all'); setUseCase('all'); setCategory('all'); setInstitution('all'); setCountry('all'); setDegree('all'); setCompiler('all'); };
  const activeFilterCount = [tier !== 'all', language !== 'all', framework !== 'all', useCase !== 'all', category !== 'all', institution !== 'all', country !== 'all', degree !== 'all', compiler !== 'all'].filter(Boolean).length + (query.trim() ? 1 : 0);

  return <>
    <header className="interactive-page-container interactive-page-heading interactive-template-directory-hero">
      <span className="interactive-eyebrow">Learnitive Template Library</span>
      <h1 className="interactive-display">Start with a document that fits your writing.</h1>
      <p>Choose Markdown for everyday writing or LaTeX for academic and technical documents, then preview a local-first Learnitive starting point.</p>
      <div className="interactive-template-hero-search"><Search className="size-5" aria-hidden="true"/><Input value={query} onChange={(event) => setQuery(event.currentTarget.value)} placeholder="Search articles, notes, books, theses, reports…" aria-label="Search project templates"/></div>
    </header>

    <section className="interactive-page-container interactive-template-directory-section interactive-template-fresh-section" data-testid="fresh-starting-points">
      <div className="interactive-template-directory-heading"><div><span className="interactive-eyebrow">Start here</span><h2>Fresh starting points</h2><p>Focused Markdown and LaTeX starting points. Choose one and start writing immediately.</p></div></div>
      {error ? <ErrorNotice>{error}</ErrorNotice> : null}
      <div className="interactive-card-grid interactive-template-grid interactive-template-featured-grid">{loading ? <LoadingCard/> : displayedFreshStartingPoints.map((template) => <TemplateCard key={template.id} template={template} institutions={institutions} onQuickPreview={setQuickPreview}/>)}</div>
    </section>

    <section className="interactive-page-container interactive-template-directory-section interactive-template-all-section" id="all-templates">
      <div className="interactive-template-directory-heading"><div><span className="interactive-eyebrow">Full catalog</span><h2>All templates</h2><p>Filter the launch library by document type, institution, access level and LaTeX requirements.</p></div><div className="interactive-template-sort"><Label htmlFor="template-sort">Sort</Label><select id="template-sort" value={sort} onChange={(event)=>setSort(event.currentTarget.value as typeof sort)}><option value="verified">Recently verified</option><option value="updated">Recently updated</option><option value="az">A–Z</option></select></div></div>
      <div className="interactive-template-browser">
        <aside className="interactive-template-filter-panel" aria-label="Template filters">
          <div className="interactive-template-filter-panel-head"><strong>Filters{activeFilterCount ? ` (${activeFilterCount})` : ''}</strong>{activeFilterCount ? <button type="button" onClick={clearFilters}>Clear</button> : null}</div>
          <div><Label htmlFor="template-filter-search">Search</Label><Input id="template-filter-search" value={query} onChange={(event)=>setQuery(event.currentTarget.value)} placeholder="Document type, institution or keyword"/></div>
          <div><Label htmlFor="template-filter-language">Language</Label><select id="template-filter-language" value={language} onChange={(event)=>setLanguage(event.currentTarget.value)}><option value="all">All languages</option>{languages.map((value)=><option key={value} value={value}>{value}</option>)}</select></div>
          <div><Label htmlFor="template-filter-framework">Framework</Label><select id="template-filter-framework" value={framework} onChange={(event)=>setFramework(event.currentTarget.value)}><option value="all">All compatible frameworks</option>{frameworks.map((value)=><option key={value} value={value}>{value}</option>)}</select></div>
          <div><Label htmlFor="template-filter-usecase">Use Case</Label><select id="template-filter-usecase" value={useCase} onChange={(event)=>setUseCase(event.currentTarget.value)}><option value="all">All use cases</option>{useCases.map((value)=><option key={String(value)} value={String(value)}>{String(value).replaceAll('-', ' ')}</option>)}</select></div>
          <div><Label>Access</Label><div className="interactive-segmented interactive-template-tier-segment">{(['all','free','pro'] as const).map((value) => <button type="button" key={value} data-active={tier===value} onClick={() => setTier(value)}>{value === 'all' ? 'All' : value === 'free' ? 'Free' : 'Pro'}</button>)}</div></div>
          <details><summary>More Filters</summary><div className="interactive-template-more-filters"><div><Label htmlFor="template-filter-category">Legacy LaTeX category</Label><select id="template-filter-category" value={category} onChange={(event)=>setCategory(event.currentTarget.value)}><option value="all">All document types</option>{browseCategories.map((item)=><option key={item.id} value={item.id}>{item.name}</option>)}</select></div>
          <div><Label htmlFor="template-filter-institution">Institution</Label><select id="template-filter-institution" value={institution} onChange={(event)=>setInstitution(event.currentTarget.value)}><option value="all">All institutions</option>{institutions.filter((item)=>(institutionCounts.get(item.id)||0)>0).map((value)=><option key={value.id} value={value.id}>{value.displayName}</option>)}</select></div>
          <div><Label htmlFor="template-filter-country">Country</Label><select id="template-filter-country" value={country} onChange={(event)=>setCountry(event.currentTarget.value)}><option value="all">All countries</option>{countries.map((value)=><option key={value} value={value}>{value}</option>)}</select></div>
          <div><Label htmlFor="template-filter-degree">Degree</Label><select id="template-filter-degree" value={degree} onChange={(event)=>setDegree(event.currentTarget.value)}><option value="all">All degrees</option>{degrees.map((value)=><option key={value} value={value}>{value}</option>)}</select></div>
          <div><Label htmlFor="template-filter-compiler">LaTeX compiler</Label><select id="template-filter-compiler" value={compiler} onChange={(event)=>setCompiler(event.currentTarget.value)}><option value="all">All compilers</option>{compilers.map((value)=><option key={value} value={value}>{value}</option>)}</select></div></div></details>
        </aside>
        <div className="interactive-template-results">
          <div className="interactive-template-filter-summary" aria-live="polite"><span>{loading ? 'Loading templates…' : sorted.length ? `${pageStart + 1}–${Math.min(pageStart + TEMPLATE_PAGE_SIZE, sorted.length)} of ${sorted.length} matching · ${templates.length} total` : `0 of ${templates.length} templates`}</span>{activeFilterCount ? <span>Filtered catalog</span> : <span>Complete catalog</span>}</div>
          <div className="interactive-card-grid interactive-template-grid">{loading ? <LoadingCard/> : pageRows.map((template)=><TemplateCard key={template.id} template={template} institutions={institutions} onQuickPreview={setQuickPreview}/>)}{!loading && sorted.length === 0 ? <div className="interactive-empty-state"><h2>No templates match</h2><p>Clear one or more filters or search using a broader term.</p><Button variant="outline" onClick={clearFilters}>Clear filters</Button></div> : null}</div>
          {!loading && sorted.length > TEMPLATE_PAGE_SIZE ? <nav className="interactive-template-pagination" aria-label="Template catalog pagination"><button type="button" disabled={currentPage<=1} onClick={()=>goToTemplatePage(currentPage-1)}>← Previous</button><div>{Array.from({length:totalPages},(_,index)=>index+1).filter((value)=>value===1||value===totalPages||Math.abs(value-currentPage)<=1).map((value,index,array)=><React.Fragment key={value}>{index>0&&value-(array[index-1]??value)>1?<span aria-hidden="true">…</span>:null}<button type="button" aria-current={value===currentPage?'page':undefined} data-active={value===currentPage} onClick={()=>goToTemplatePage(value)}>{value}</button></React.Fragment>)}</div><button type="button" disabled={currentPage>=totalPages} onClick={()=>goToTemplatePage(currentPage+1)}>Next →</button></nav> : null}
        </div>
      </div>
    </section>

    <section className="interactive-page-container interactive-template-missing-cta"><div><span className="interactive-eyebrow">Bring your own project</span><h2>Can't find the template you need?</h2><p>Import a university or publisher ZIP into Learnitive, or start a clean local first project and keep writing.</p></div><div className="interactive-template-missing-actions"><a className="interactive-button interactive-button-primary" href="/projects">Import ZIP</a><a className="interactive-button interactive-button-secondary" href="/projects">Start Blank Project</a></div></section>

    <TemplateQuickPreviewDialog template={quickPreview} institutions={institutions} onClose={()=>setQuickPreview(null)}/>
  </>;
}

const DEFAULT_TEMPLATE_THUMBNAIL_URL = '/learnitive-template-default-thumbnail.webp';

function templateCoverIcon(template: ProjectTemplateDefinition): TechnologyIconSpec {
  const mode = projectModeById(template.projectModeId);
  const language = mode ? projectLanguageById(mode.primaryLanguageId) : undefined;
  return resolveTechnologyIcon({
    framework: template.framework || mode?.framework,
    languageId: language?.id,
    languageLabel: template.primaryLanguage || language?.label,
    fallback: language?.iconText,
  });
}

function TemplateTechnologyIcon({ spec, compact = false }: { spec: TechnologyIconSpec; compact?: boolean }) {
  const [failed, setFailed] = React.useState(false);
  const [loaded, setLoaded] = React.useState(false);
  return <span className="interactive-template-cover-icon" title={spec.label}>
    <b aria-hidden="true" data-hidden={loaded ? 'true' : 'false'}>{spec.fallback}</b>
    {spec.src && !failed ? <img src={spec.src} alt="" aria-hidden="true" loading={compact ? 'lazy' : 'eager'} decoding="async" referrerPolicy="no-referrer" data-loaded={loaded ? 'true' : 'false'} onLoad={()=>setLoaded(true)} onError={()=>{setFailed(true);setLoaded(false)}}/> : null}
  </span>;
}

function TemplateWireframe({ template, compact = false }: { template: ProjectTemplateDefinition; compact?: boolean }) {
  const language = template.primaryLanguage?.trim() || 'Project';
  const icon = templateCoverIcon(template);
  return (
    <div className={`interactive-template-wireframe interactive-template-cover${compact ? ' is-compact' : ''}`} role="img" aria-label={`${template.name} ${language} template cover`}>
      <div className="interactive-template-wireframe-topbar">
        <span className="interactive-template-wireframe-dots" aria-hidden="true"><i/><i/><i/></span>
        <strong>{language}</strong>
        <span>Learnitive</span>
      </div>
      <div className="interactive-template-cover-body" aria-hidden="true">
        <TemplateTechnologyIcon spec={icon} compact={compact}/>
      </div>
    </div>
  );
}

function TemplatePreviewSurface({ template, compact = false }: { template: ProjectTemplateDefinition; compact?: boolean }) {
  return <TemplateWireframe template={template} compact={compact}/>;
}

function TemplateCard({ template, institutions, onQuickPreview }: { template: ProjectTemplateDefinition; institutions?: readonly ProjectTemplateInstitution[]; onQuickPreview: (template: ProjectTemplateDefinition) => void }) {
  const institutionName = institutions ? templateInstitutionName(template, institutions) : '';
  const openPreview = () => onQuickPreview(template);
  return <div
    className="interactive-template-card interactive-template-card-clickable"
    role="button"
    tabIndex={0}
    aria-label={`Preview ${template.name}`}
    onClick={openPreview}
    onKeyDown={(event) => {
      if (event.key === 'Enter' || event.key === ' ') {
        event.preventDefault();
        openPreview();
      }
    }}
  >
    <div className="interactive-template-card-preview-link" aria-hidden="true">
      <div className="interactive-template-card-preview-chrome">
        <span className="interactive-template-card-preview-dots"><i/><i/><i/></span>
        <TerminalSquare className="interactive-template-card-preview-terminal"/>
      </div>
      <TemplatePreviewSurface template={template} compact/>
    </div>
    <div className="interactive-template-card-body">
      <div className="interactive-card-meta"><span>{template.primaryLanguage}</span>{template.framework ? <span>{template.framework}</span> : null}<span>{template.useCaseId.replaceAll('-', ' ')}</span></div>
      <h2>{template.name}</h2>
      {institutionName ? <p className="interactive-template-institution"><strong>{institutionName}</strong>{template.degreeLevels.length ? ` · ${template.degreeLevels.join(', ')}` : ''}</p> : null}
      <p>{template.description}</p>
      <div className="interactive-template-mini-meta"><span>{template.projectType}</span>{template.validationStatus === 'valid' ? <span>Validated</span> : null}{template.compiler ? <span>{template.compiler}</span> : null}</div>
    </div>
  </div>;
}

function TemplateQuickPreviewDialog({ template, institutions, onClose }: { template: ProjectTemplateDefinition | null; institutions: readonly ProjectTemplateInstitution[]; onClose: () => void }) {
  const institution = template?.institution ? institutions.find((item)=>item.id===template.institution) : undefined;
  return <Dialog open={Boolean(template)} onOpenChange={(open)=>{if(!open)onClose()}}><DialogContent className="interactive-template-quick-dialog">{template ? <>
    <DialogHeader><DialogTitle>{template.name}</DialogTitle><DialogDescription>{institution ? `${institution.displayName}${template.degreeLevels.length ? ` · ${template.degreeLevels.join(', ')}` : ''}` : template.category || 'Uncategorised'}</DialogDescription></DialogHeader>
    <div className="interactive-template-quick-layout"><div className="interactive-template-quick-preview"><TemplatePreviewSurface template={template}/></div><div className="interactive-template-quick-copy"><div className="interactive-card-meta"><span>{template.primaryLanguage}</span>{template.framework?<span>{template.framework}</span>:null}<span>{template.useCaseId.replaceAll('-', ' ')}</span></div><p>{template.description}</p><dl className="interactive-template-quick-facts">{institution?<div><dt>Institution</dt><dd>{institution.displayName}</dd></div>:null}{template.compiler?<div><dt>Compiler</dt><dd>{template.compiler}</dd></div>:null}{template.documentClass?<div><dt>Class</dt><dd>{template.documentClass}</dd></div>:null}{template.lastVerified?<div><dt>Verified</dt><dd>{template.lastVerified}</dd></div>:null}</dl><div className="interactive-template-quick-actions"><a className="interactive-button interactive-button-secondary" href={`/templates/${template.slug}`}>View Details</a><ApplyTemplateButton template={template}/></div></div></div>
  </>:null}</DialogContent></Dialog>;
}

function TemplatesInstitutionsPage() {
  const { templates, institutions, loading, error } = useTemplates();
  const [query, setQuery] = React.useState('');
  const [country, setCountry] = React.useState('all');
  const counts = new Map<string, number>();
  for (const template of templates) if (template.institution) counts.set(template.institution, (counts.get(template.institution)||0)+1);
  const institutionsWithTemplates = institutions.filter((item)=>(counts.get(item.id)||0)>0);
  const countries = [...new Set(institutionsWithTemplates.map((item)=>item.country).filter((value): value is string => Boolean(value)))].sort();
  const needle = query.trim().toLowerCase();
  const rows = institutionsWithTemplates.map((institution)=>({institution,count:counts.get(institution.id)||0})).filter(({institution})=>(country==='all'||institution.country===country) && (!needle || `${institution.name} ${institution.displayName} ${institution.shortName||''} ${(institution.aliases||[]).join(' ')} ${institution.country||''} ${institution.region||''}`.toLowerCase().includes(needle))).sort((a,b)=>a.institution.country?.localeCompare(b.institution.country||'')||a.institution.displayName.localeCompare(b.institution.displayName));
  const grouped = rows.reduce<Map<string, typeof rows>>((map,row)=>{const key=row.institution.country||'Other';const group=map.get(key)||[];group.push(row);map.set(key,group);return map},new Map());
  return <>
    <MarketingPageHeading eyebrow="Academic institutions" title="Find LaTeX templates by university." copy="Browse institution-classified thesis, dissertation, report and academic templates. Learnitive keeps provenance explicit so demo, community or adapted projects are never presented as official by accident."/>
    <section className="interactive-page-container interactive-institution-directory">
      <div className="interactive-institution-directory-controls"><div className="interactive-template-hero-search"><Search className="size-5" aria-hidden="true"/><Input value={query} onChange={(event)=>setQuery(event.currentTarget.value)} placeholder="Search universities or regions…"/></div><select aria-label="Institution country" value={country} onChange={(event)=>setCountry(event.currentTarget.value)}><option value="all">All countries</option>{countries.map((value)=><option key={value} value={value}>{value}</option>)}</select></div>
      {error?<ErrorNotice>{error}</ErrorNotice>:null}
      <div className="interactive-template-filter-summary" aria-live="polite"><span>{loading?'Loading institutions…':`${rows.length} institutions with templates`}</span><a className="interactive-text-link" href="/templates">Back to all templates <ChevronRight className="size-4"/></a></div>
      {loading?<LoadingCard/>:[...grouped.entries()].map(([countryName,countryRows])=><section className="interactive-institution-country-group" key={countryName}><div className="interactive-template-directory-heading"><div><span className="interactive-eyebrow">Country</span><h2>{countryName}</h2></div></div><div className="interactive-template-institution-strip">{countryRows.map(({institution,count})=><a className="interactive-template-institution-card" key={institution.id} href={`/templates/institutions/${institution.slug}`}><InstitutionMark institution={institution}/><div><h3>{institution.displayName}</h3><p>{institution.region||institution.country||'Academic institution'}</p><strong>{count} {count===1?'template':'templates'}</strong></div><ChevronRight className="size-4" aria-hidden="true"/></a>)}</div></section>)}
      {!loading&&rows.length===0?<div className="interactive-empty-state"><h2>No institutions match</h2><p>Try a broader university name or country.</p></div>:null}
    </section>
  </>;
}

function TemplatesCategoryPage({ slug }: { slug: string }) {
  const { templates, institutions, categories, loading, error } = useTemplates();
  const launchTemplates = React.useMemo(() => templates.filter((template) => template.projectModeId === 'markdown' || template.projectModeId === 'latex'), [templates]);
  const [quickPreview,setQuickPreview]=React.useState<ProjectTemplateDefinition|null>(null);
  const category=categories.find((item)=>item.slug===slug);
  const rows=category?templates.filter((template)=>template.categoryId===category.id):[];
  usePageMetadata({title:category?`${category.name} LaTeX Templates`:'LaTeX Template Category',description:category?(category.description||`Browse ${category.name} LaTeX templates in Learnitive.`):'Browse Learnitive LaTeX template categories.',pathname:`/templates/categories/${encodeURIComponent(slug)}`, enabled:Boolean(category)});
  if(loading)return <MarketingPageHeading eyebrow="Templates" title="Loading category…" copy=""/>;
  if(error||!category||rows.length===0)return <MarketingPageHeading eyebrow="Templates" title="Template category not found" copy={error||'This template category has no published templates in the current catalog.'}/>;
  return <>
    <MarketingPageHeading eyebrow={`${rows.length} ${rows.length===1?'template':'templates'}`} title={`${category.name} LaTeX Templates`} copy={category.description||`Browse ${category.name} templates and open a complete project in Learnitive.`}/>
    <section className="interactive-page-container"><div className="interactive-template-filter-summary"><a className="interactive-text-link" href="/templates">All templates <ChevronRight className="size-4"/></a><a className="interactive-text-link" href="/templates/institutions">Browse universities <ChevronRight className="size-4"/></a></div><div className="interactive-card-grid interactive-template-grid">{rows.map((template)=><TemplateCard key={template.id} template={template} institutions={institutions} onQuickPreview={setQuickPreview}/>)}</div></section><TemplateQuickPreviewDialog template={quickPreview} institutions={institutions} onClose={()=>setQuickPreview(null)}/>
  </>;
}

function TemplateInstitutionPage({ slug }: { slug: string }) {
  const { templates, institutions, loading, error } = useTemplates();
  const [degree,setDegree]=React.useState('all');
  const [quickPreview,setQuickPreview]=React.useState<ProjectTemplateDefinition|null>(null);
  const institution = institutions.find((item) => item.slug === slug);
  const allRows = institution ? templates.filter((template) => template.institution === institution.id) : [];
  const degrees=[...new Set(allRows.flatMap((template)=>[...template.degreeLevels]))].sort();
  const rows=degree==='all'?allRows:allRows.filter((template)=>template.degreeLevels.includes(degree));
  const lastVerified=[...allRows].map((template)=>template.lastVerified||template.updatedAt||'').filter(Boolean).sort().at(-1);
  usePageMetadata({
    title: institution ? `${institution.displayName} LaTeX Templates` : 'Academic Institution Templates',
    description: institution?.description || (institution ? `Browse ${institution.displayName} LaTeX templates in Learnitive.` : 'Browse academic institution LaTeX templates in Learnitive.'),
    pathname: `/templates/institutions/${encodeURIComponent(slug)}`,
    enabled: Boolean(institution),
  });
  if (loading) return <MarketingPageHeading eyebrow="Academic institution" title="Loading templates…" copy="" />;
  if (error || !institution || allRows.length === 0) return <MarketingPageHeading eyebrow="Academic institution" title="Institution not found" copy={error || 'This institution has no published templates in the current catalog.'} />;
  return <>
    <MarketingPageHeading eyebrow={institution.country || 'Academic institution'} title={`${institution.displayName} LaTeX Templates`} copy={institution.description || `Templates classified for ${institution.displayName}. Check each template's provenance, licence and verification details before use.`}/>
    <section className="interactive-page-container interactive-institution-template-page">
      {institution.logoUrl ? <div className="interactive-institution-detail-logo"><img src={institution.logoUrl} alt={`${institution.displayName} logo`} /></div> : null}
      <div className="interactive-institution-detail-meta"><span>{allRows.length} {allRows.length===1?'template':'templates'}</span>{institution.region?<span>{institution.region}</span>:null}{lastVerified?<span>Latest verification {lastVerified}</span>:null}{institution.website?<a className="interactive-text-link" href={institution.website} target="_blank" rel="noopener noreferrer">Institution website <ExternalLink className="size-3"/></a>:null}</div>
      {degrees.length?<nav className="interactive-template-intent-chips" aria-label="Degree level"><button type="button" data-active={degree==='all'} onClick={()=>setDegree('all')}>All</button>{degrees.map((value)=><button type="button" data-active={degree===value} key={value} onClick={()=>setDegree(value)}>{value}</button>)}</nav>:null}
      <div className="interactive-card-grid interactive-template-grid">{rows.map((template)=><TemplateCard key={template.id} template={template} institutions={institutions} onQuickPreview={setQuickPreview}/>)}{rows.length===0?<div className="interactive-empty-state"><p>No published templates match this degree filter.</p></div>:null}</div>
      <aside className="interactive-template-provenance-note"><strong>Institution classification</strong><p>Templates in this directory can be official, adapted, community-created, or Learnitive-created. Check each template detail page before treating it as an institution-approved submission format.</p></aside>
    </section>
    <TemplateQuickPreviewDialog template={quickPreview} institutions={institutions} onClose={()=>setQuickPreview(null)}/>
  </>;
}

function TemplateDetailPage({ slug }: { slug: string }) {
  const { templates, institutions, loading, error } = useTemplates();
  const template = templates.find((item) => item.slug === slug);
  const institution = template?.institution ? institutions.find((item) => item.id === template.institution) : undefined;
  usePageMetadata({
    title: template?.seoTitle || template?.name || 'Template',
    description: template?.seoDescription || template?.description || 'Preview and apply a Learnitive LaTeX project template.',
    pathname: `/templates/${encodeURIComponent(slug)}`,
    image: template?.previewKind === 'image' ? template.preview || template.thumbnail || null : template?.thumbnail || null,
    enabled: Boolean(template),
  });
  if (loading) return <MarketingPageHeading eyebrow="Template" title="Loading template…" copy="" />;
  if (error || !template) return <MarketingPageHeading eyebrow="Template" title="Template not found" copy={error || 'This template is not present in the current template manifest.'} />;
  const provenance = template.provenance === 'official' ? 'Official source' : template.provenance === 'adapted' ? 'Adapted' : template.provenance === 'community' ? 'Community' : 'Learnitive-created';
  return <section className="interactive-page-container interactive-template-detail">
    <div className="interactive-template-preview"><TemplatePreviewSurface template={template} /></div>
    <div className="interactive-template-detail-copy">
      <div className="interactive-card-meta"><span>{template.category || 'Uncategorised'}</span><span className="interactive-badge">Template</span><span>{provenance}</span></div>
      <h1 className="interactive-display">{template.name}</h1>
      {institution ? <p className="interactive-template-institution"><a className="interactive-text-link" href={`/templates/institutions/${institution.slug}`}>{institution.displayName}</a>{template.degreeLevels.length ? ` · ${template.degreeLevels.join(', ')}` : ''}</p> : null}
      <p>{template.description}</p>{template.longDescription ? <p>{template.longDescription}</p> : null}
      <dl className="interactive-template-facts">
        <div><dt>Entry file</dt><dd><code>{template.entryFile}</code></dd></div>
        <div><dt>Version</dt><dd>{template.templateVersion}</dd></div>
        <div><dt>Author</dt><dd>{template.originalAuthor || template.author}</dd></div>
        {template.compiler ? <div><dt>Compiler</dt><dd>{template.compiler}</dd></div> : null}
        {template.bibliographyEngine ? <div><dt>Bibliography</dt><dd>{template.bibliographyEngine}</dd></div> : null}
        {template.documentClass ? <div><dt>Document class</dt><dd><code>{template.documentClass}</code></dd></div> : null}
        {template.license ? <div><dt>Licence</dt><dd>{template.license}</dd></div> : null}
        {template.lastVerified ? <div><dt>Last verified</dt><dd>{template.lastVerified}</dd></div> : null}
        {template.country ? <div><dt>Country</dt><dd>{template.country}</dd></div> : null}
      </dl>
      {template.features.length ? <section className="interactive-template-features"><h2>Template features</h2><ul>{template.features.map((feature) => <li key={feature}><Check className="size-4" /> {feature}</li>)}</ul></section> : null}
      {template.requiredPackages.length ? <section className="interactive-template-packages"><h2>Required packages</h2><div className="interactive-tag-row">{template.requiredPackages.map((pkg) => <span key={pkg}>{pkg}</span>)}</div></section> : null}
      {template.tags.length ? <div className="interactive-tag-row">{template.tags.map((tag) => <span key={tag}>{tag}</span>)}</div> : null}
      {template.sourceUrl ? <p><a className="interactive-text-link" href={template.sourceUrl} target="_blank" rel="noopener noreferrer">Template source <ExternalLink className="size-3" /></a></p> : null}
      {template.attribution ? <p className="interactive-muted">{template.attribution}</p> : null}
      <ApplyTemplateButton template={template} />
    </div>
  </section>;
}

function ApplyTemplateButton({ template }: { template: ProjectTemplateDefinition }) {
  const core = useCoreServices();
  const [busy, setBusy] = React.useState(false);
  const apply = async () => {
    setBusy(true);
    try {
      const preparedFiles = await fetchProjectTemplateFiles(template);
      const baseName = template.name.trim() || template.slug;
      const name = await uniqueWorkspaceName(baseName, core.workspaceOps);
      const type: WorkspaceStorageType = supportsOpfsProjectStorage()
        ? WORKSPACE_STORAGE_TYPE.BrowserOPFS
        : WORKSPACE_STORAGE_TYPE.Browser;
      const mode = projectModeById(template.projectModeId) ?? projectModeById('latex');
      if (!mode) throw new Error('This template uses an unsupported project mode.');
      const profile = canonicalProfileForMode(mode.id);
      const learnitiveConfig = mode.id === 'latex' ? (template.config ?? createDefaultLearnitiveProjectConfig()) : undefined;
      if (learnitiveConfig) learnitiveConfig.rootFile = template.entryFile;
      await core.workspaceOps.createWorkspaceInfo({
        name,
        type,
        metadata: {
          ...(learnitiveConfig ? { project: learnitiveConfig } : {}),
          projectProfile: profile,
          projectModeId: mode.id,
          product: 'learnitive',
          projectDisplayName: baseName,
          templateSource: { id: template.id, version: template.templateVersion, tier: template.tier },
        },
      });
      try {
        await requestPersistentBrowserStorage();
        const provider = await core.projectStorage.getProvider(name);
        await writePreparedProjectTemplateFiles(provider, template, preparedFiles);
        await finalizeCreatedBrowserProject(name, core);
      } catch (error) {
        await core.workspaceOps.deleteWorkspaceInfo(name).catch(() => undefined);
        throw error;
      }
      await core.workspaceState.refreshWorkspacesNow();
      window.location.href = core.navigation.toUri({ route: 'editor', payload: { wsPath: WsPath.fromParts(name, template.entryFile).wsPath } });
    } catch (reason) {
      toast.error(reason instanceof Error ? reason.message : String(reason));
    } finally { setBusy(false); }
  };
  return <Button className="interactive-apply-button" disabled={busy} onClick={() => void apply()}>{busy ? 'Creating project…' : 'Apply Template'}</Button>;
}

async function directoryIsEmpty(handle: FileSystemDirectoryHandle): Promise<boolean> {
  const iterator = (handle as FileSystemDirectoryHandle & { values?: () => AsyncIterableIterator<FileSystemHandle> }).values?.();
  if (!iterator) return false;
  for await (const _entry of iterator) return false;
  return true;
}

async function chooseProjectDirectory(parent: FileSystemDirectoryHandle, name: string): Promise<FileSystemDirectoryHandle> {
  if (await directoryIsEmpty(parent)) return parent;
  const safe = normalizeProjectSlug(name, { fallback: 'learnitive-project', maxLength: 112 });
  let candidate = safe;
  for (let index = 1; index < 100; index += 1) {
    try { return await parent.getDirectoryHandle(candidate, { create: true }); }
    catch { candidate = normalizeProjectSlug(`${safe}-${index + 1}`, { fallback: 'learnitive-project', maxLength: 120 }); }
  }
  throw new Error('Unable to create a project directory in the selected folder.');
}

async function uniqueWorkspaceName(base: string, workspaceOps: ReturnType<typeof useCoreServices>['workspaceOps']): Promise<string> {
  const clean = normalizeProjectSlug(base, { fallback: 'project', maxLength: 112 });
  for (let index = 0; index < 1000; index += 1) {
    const candidate = index === 0 ? clean : normalizeProjectSlug(`${clean}-${index + 1}`, { fallback: 'project', maxLength: 120 });
    const validation = WsPath.safeFromParts(candidate, '');
    if (!validation.ok) continue;
    if (!(await workspaceOps.getWorkspaceInfo(candidate, { allowDeleted: true }))) return candidate;
  }
  throw new Error('Unable to choose a unique project slug.');
}

function PricingPage() {
  const { config } = useSaasSession();
  return <>
    <MarketingPageHeading eyebrow="Pricing" title="A mindful pricing." copy="" />
    <LearnitivePricingTable variant="full" lifetimeShopUrl={config?.shopUrl} monthlyShopUrl={config?.monthlyShopUrl} freeProjectLimit={config?.freePlanProjectLimit} />
    <PricingComparisonTable freeProjectLimit={config?.freePlanProjectLimit} />
    <PublicFaq title="Pricing FAQ" items={PRICING_FAQ} freeProjectLimit={config?.freePlanProjectLimit} />
    <PublicFinalCta heading="Start free. Upgrade when Pro is useful." copy="Your project files remain yours on every plan." primaryLabel="Open Projects" primaryHref="/projects" secondaryLabel="Explore Features" secondaryHref="/features" />
  </>;
}

function LoginPage({ register }: { register: boolean }) {
  const { auth, config, loading } = useSaasSession();
  const returnTo = React.useMemo(
    () => safeClientReturnPath(new URLSearchParams(window.location.search).get('returnTo')),
    [],
  );

  React.useEffect(() => {
    if (!loading && auth.authenticated) window.location.replace(returnTo);
  }, [auth.authenticated, loading, returnTo]);

  return (
    <section className="interactive-auth-page interactive-auth-page--login">
      <InteractiveAuthCard
        config={config}
        returnTo={returnTo}
        eyebrow={register ? 'Create account' : 'Welcome back'}
        title={register ? 'Create account' : 'Sign in'}
        description={
          returnTo === '/apply'
            ? 'Sign in, then Learnitive will return you to the license page.'
            : 'Continue with Google, GitHub or a secure email link'
        }
      />
    </section>
  );
}

function MagicLoginPage() {
  const token = React.useMemo(() => new URLSearchParams(window.location.search).get('token') || '', []);
  const [state, setState] = React.useState<'ready' | 'working' | 'error'>('ready');
  const [message, setMessage] = React.useState('');
  const consume = async () => {
    if (!token) { setState('error'); setMessage('This sign in link is invalid or incomplete.'); return; }
    setState('working'); setMessage('');
    try {
      const result = await consumeMagicLogin(token);
      window.location.replace(safeClientReturnPath(result.returnTo));
    } catch (reason) {
      const code = reason instanceof SaasApiError ? reason.code : '';
      setState('error');
      setMessage(code === 'magic_link_expired' || code === 'magic_link_used' ? 'This sign in link has expired or was already used.' : 'This sign in link could not be verified.');
    }
  };
  return <section className="interactive-auth-page"><div className="interactive-auth-card"><Brand/><span className="interactive-eyebrow">Email sign in</span><h1 className="interactive-display">Continue to Learnitive</h1><p>This one time link will sign you in</p>{state === 'error' ? <ErrorNotice>{message}</ErrorNotice> : null}<Button type="button" disabled={state === 'working'} onClick={() => void consume()}>{state === 'working' ? <><LoaderCircle className="size-4 animate-spin"/> Signing in…</> : 'Continue'}</Button><a className="interactive-text-link" href="/login">Request another link</a></div></section>;
}

function ShareProjectPage({ token }: { token: string }) {
  const { auth, loading } = useSaasSession();
  const core = useCoreServices();
  const [state, setState] = React.useState<{ project?: CloudProject; permission?: 'view' | 'edit'; workspace?: WorkspaceInfo; previewUrl?: string; error?: string }>({});
  React.useEffect(() => {
    if (loading) return;
    if (!auth.authenticated) {
      const returnTo = `${window.location.pathname}${window.location.search}${window.location.hash}`;
      window.location.replace(`/login?returnTo=${encodeURIComponent(returnTo)}`);
      return;
    }
    let active = true;
    void (async () => {
      try {
        const redeemed = await redeemProjectShareToken(token);
        const details = await getCloudProject(redeemed.projectId);
        const workspaces = await core.workspaceOps.getAllWorkspaces();
        const workspace = workspaces.find((item) => workspaceCloudId(item) === redeemed.projectId);
        const processes = await listProjectSandboxProcesses(redeemed.projectId).catch(() => []);
        const live = processes.find((item) => item.status === 'running' && item.port && (item.processKey === 'dev' || item.processKey === 'preview'));
        const preview = live ? await getProjectSandboxPreviewAccess(redeemed.projectId, live.processKey).catch(() => undefined) : undefined;
        if (active) setState({ project: details.project, permission: redeemed.permission, workspace, ...(preview ? { previewUrl: preview.url } : {}) });
      } catch (reason) {
        if (active) setState({ error: reason instanceof Error ? reason.message : 'This share link is invalid, expired or revoked.' });
      }
    })();
    return () => { active = false; };
  }, [auth.authenticated, core.workspaceOps, loading, token]);

  if (loading || (!state.project && !state.error)) return <LoadingCard />;
  if (state.error) return <div className="interactive-empty-state"><FileCode2 className="size-7"/><h1 className="interactive-display">Shared project unavailable</h1><p>{state.error}</p><a className="interactive-button interactive-button-secondary" href="/projects">Projects</a></div>;
  const project = state.project!;
  const href = state.workspace ? workspaceOpenHref(core, state.workspace) : undefined;
  return <section className="interactive-page-container space-y-5">
    <div><span className="interactive-eyebrow">Authenticated project share</span><h1 className="interactive-display">{project.name}</h1><p>Access granted as <strong>{state.permission === 'edit' ? 'editor' : 'viewer'}</strong>. This share authorization is checked server-side and stops working if the owner revokes the link.</p></div>
    {href ? <a className="interactive-button interactive-button-primary" href={href}>Open local project</a> : null}
    {state.previewUrl ? <div className="overflow-hidden rounded-lg border bg-white"><iframe className="min-h-[60vh] w-full" sandbox="allow-scripts allow-forms allow-modals allow-downloads" src={state.previewUrl} title={`${project.name} preview`}/></div> : <div className="rounded-lg border bg-muted/20 p-4 text-sm text-muted-foreground"><strong className="text-foreground">No live Preview is currently available.</strong><p className="mt-1">Learnitive keeps durable source local to its owner. A shared live Preview becomes available when the owner has the project materialized in an active sandbox and a project server is running.</p></div>}
  </section>;
}

function useCmsList(loader: () => Promise<CmsListItem[]>) {
  const [items, setItems] = React.useState<CmsListItem[]>([]); const [loading,setLoading]=React.useState(true); const [error,setError]=React.useState('');
  React.useEffect(() => { let active=true; void loader().then((value)=>{if(active)setItems(value)}).catch((reason)=>{if(active)setError(reason instanceof Error?reason.message:String(reason))}).finally(()=>{if(active)setLoading(false)}); return()=>{active=false}; }, [loader]);
  return {items,loading,error};
}

function blogDate(value?: string) {
  if (!value) return '';
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? '' : date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
}

function BlogCover({ item, featured = false }: { item: CmsListItem; featured?: boolean }) {
  if (!item.cover_image_url) return <div className={`interactive-blog-cover interactive-blog-cover-placeholder${featured ? ' interactive-blog-cover-featured' : ''}`} aria-hidden><FileText className="size-9" /></div>;
  return <div className={`interactive-blog-cover${featured ? ' interactive-blog-cover-featured' : ''}`}><img src={item.cover_image_url} alt={item.cover_image_alt || ''} width={item.cover_image_width || undefined} height={item.cover_image_height || undefined} loading={featured ? 'eager' : 'lazy'} /></div>;
}

function BlogPostCard({ item, featured = false }: { item: CmsListItem; featured?: boolean }) {
  return <a className={`interactive-blog-card${featured ? ' interactive-blog-card-featured' : ''}`} href={`/blog/${item.slug}`} aria-label={`Read blog post: ${item.title}`}>
    <BlogCover item={item} featured={featured} />
    <article className="interactive-blog-card-body">
      <div className="interactive-blog-card-meta"><time>{blogDate(item.published_at)}</time>{item.tags?.length ? <span className="interactive-blog-mini-tags">{item.tags.slice(0, featured ? 3 : 2).map((tag) => <span key={tag}>{tag}</span>)}</span> : null}</div>
      <div className="interactive-blog-card-copy"><h2>{item.title}</h2>{item.excerpt ? <p>{item.excerpt}</p> : null}</div>
      {item.author ? <div className="interactive-blog-author">{item.author_avatar_url ? <img className="interactive-blog-author-avatar" src={item.author_avatar_url} alt="" referrerPolicy="no-referrer" /> : <span className="interactive-blog-author-avatar" aria-hidden>{item.author.trim().slice(0, 1).toUpperCase()}</span>}<span>{item.author}</span></div> : null}
    </article>
  </a>;
}


function changeLogDate(value?: string) {
  if (!value) return '';
  const parsed = new Date(`${value.slice(0, 10)}T00:00:00`);
  return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
}

function ChangeLogPage() {
  const [page, setPage] = React.useState<ChangeLogPageSettings>({ title: 'Change log', intro_html: '' });
  const [items, setItems] = React.useState<ChangeLogEvent[]>([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState('');
  React.useEffect(() => {
    let active = true;
    void getChangeLog()
      .then((result) => { if (active) { setPage(result.page); setItems(result.items); } })
      .catch((reason) => { if (active) setError(reason instanceof Error ? reason.message : String(reason)); })
      .finally(() => { if (active) setLoading(false); });
    return () => { active = false; };
  }, []);
  return <section className="interactive-page-container interactive-change-log-page">
    <header className="interactive-change-log-heading"><span className="interactive-eyebrow">Product updates</span><h1 className="interactive-display">{page.title || 'Change log'}</h1>{page.intro_html ? <DocsSanitizedHtml html={page.intro_html} /> : <p>Recent improvements, fixes and new capabilities in Learnitive.</p>}</header>
    {error ? <ErrorNotice>{error}</ErrorNotice> : null}
    {loading ? <LoadingCard /> : items.length ? <div className="interactive-change-log-timeline">{items.map((item) => <article className="interactive-change-log-event" key={item.id}>
      <div className="interactive-change-log-date"><CalendarDays className="size-4" aria-hidden="true"/><time dateTime={item.change_date}>{changeLogDate(item.change_date)}</time></div>
      <div className="interactive-change-log-card"><h2>{item.title}</h2>{item.content_html ? <DocsSanitizedHtml html={item.content_html} /> : null}</div>
    </article>)}</div> : <div className="interactive-empty-state"><p>No published changes yet.</p></div>}
  </section>;
}

function BlogPage() {
  const initialBlogParams = React.useMemo(() => new URLSearchParams(window.location.search), []);
  const [query, setQuery] = React.useState(() => initialBlogParams.get('search') || '');
  const [tag, setTag] = React.useState(() => initialBlogParams.get('tag') || '');
  const [page, setPage] = React.useState(() => Math.max(1, Number(initialBlogParams.get('page') || 1) || 1));
  const [result, setResult] = React.useState<{ items: CmsListItem[]; page: number; pageSize: number; total: number; tags: BlogTagSummary[] }>({ items: [], page: 1, pageSize: 18, total: 0, tags: [] });
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState('');
  React.useEffect(() => {
    let active = true;
    const timer = window.setTimeout(() => {
      setLoading(true); setError('');
      void listBlogPage({ search: query, tag, page, pageSize: 18 })
        .then((value) => { if (active) setResult(value); })
        .catch((reason) => { if (active) setError(reason instanceof Error ? reason.message : String(reason)); })
        .finally(() => { if (active) setLoading(false); });
    }, 180);
    return () => { active = false; window.clearTimeout(timer); };
  }, [query, tag, page]);
  const totalPages = Math.max(1, Math.ceil(result.total / result.pageSize));
  const featured = result.items[0];
  const rest = result.items.slice(1);
  return <section className="interactive-page-container interactive-blog-page">
    <header className="interactive-blog-heading">
      <div><h1>Blog</h1><p>Articles, product updates, scientific-writing guides and Learnitive news.</p></div>
      <label className="interactive-blog-search"><Search className="size-5" /><span className="sr-only">Search blog posts</span><input type="search" value={query} onChange={(event) => { setQuery(event.currentTarget.value); setPage(1); }} placeholder="Search posts…" /></label>
    </header>
    <nav className="interactive-blog-tags" aria-label="Blog tags">
      <button type="button" data-active={!tag || undefined} onClick={() => { setTag(''); setPage(1); }}>All Posts</button>
      {result.tags.map((item) => <button type="button" key={item.slug} data-active={tag === item.slug || undefined} onClick={() => { setTag(item.slug); setPage(1); }}>{item.name}</button>)}
    </nav>
    {error ? <ErrorNotice>{error}</ErrorNotice> : null}
    {loading ? <LoadingCard /> : featured ? <>
      <div className="interactive-blog-featured"><BlogPostCard item={featured} featured /></div>
      {rest.length ? <div className="interactive-blog-grid">{rest.map((item) => <BlogPostCard key={item.slug} item={item} />)}</div> : null}
      {totalPages > 1 ? <nav className="interactive-blog-pagination" aria-label="Blog pagination"><button type="button" disabled={page <= 1} onClick={() => setPage((value) => Math.max(1, value - 1))}>← Newer</button><span>Page {page} of {totalPages}</span><button type="button" disabled={page >= totalPages} onClick={() => setPage((value) => Math.min(totalPages, value + 1))}>Older →</button></nav> : null}
    </> : <EmptyCms label={query || tag ? 'No blog posts match this filter.' : 'No published blog posts yet.'} />}
  </section>;
}
function BlogDetailPage({slug}:{slug:string}) { return <CmsDetailPage kind="blog" slug={slug} />; }
function DocDetailPage({slug}:{slug:string}) { return <CmsDetailPage kind="docs" slug={slug} />; }

const DOC_CATEGORY_ORDER = [
  'Getting Started',
  'Projects & Files',
  'Editor & Writing',
  'Run, Build & Preview',
  'Terminal & Runtime',
  'AI',
  'Templates',
  'Git & Collaboration',
  'Learn',
  'LaTeX & Scientific',
  'Research',
  'Account & Plans',
  'Troubleshooting',
] as const;

function groupDocs(items: CmsListItem[]): Array<[string, CmsListItem[]]> {
  const grouped = new Map<string, CmsListItem[]>();
  for (const item of items) {
    const category = item.category?.trim() || 'Documentation';
    grouped.set(category, [...(grouped.get(category) || []), item]);
  }
  const categoryRank = new Map<string, number>(DOC_CATEGORY_ORDER.map((category, index) => [category, index]));
  return [...grouped.entries()].sort(([left], [right]) => {
    const leftRank = categoryRank.get(left) ?? DOC_CATEGORY_ORDER.length;
    const rightRank = categoryRank.get(right) ?? DOC_CATEGORY_ORDER.length;
    return leftRank - rightRank || left.localeCompare(right);
  });
}

function DocsNavigation({items,activeSlug,onNavigate}:{items:CmsListItem[];activeSlug?:string;onNavigate?:()=>void}) {
  return <nav className="interactive-docs-nav" aria-label="Documentation">{groupDocs(items).map(([category,docs])=><section key={category}><h2>{category}</h2>{docs.map((doc)=><a key={doc.slug} href={`/docs/${doc.slug}`} data-active={doc.slug===activeSlug?'true':undefined} aria-current={doc.slug===activeSlug?'page':undefined} onClick={onNavigate}>{doc.title}</a>)}</section>)}</nav>;
}

function DocsMobileNavigation({items,activeSlug}:{items:CmsListItem[];activeSlug?:string}) {
  const [open,setOpen]=React.useState(false);
  const [query,setQuery]=React.useState('');
  const needle=query.trim().toLowerCase();
  const filtered=needle?items.filter((item)=>[item.title,item.category,item.excerpt,...(item.tags||[])].filter(Boolean).some((value)=>String(value).toLowerCase().includes(needle))):items;
  return <div className="interactive-docs-mobile-navigation"><Button variant="outline" type="button" onClick={()=>setOpen(true)} aria-haspopup="dialog"><Menu className="size-4"/> Browse docs</Button><Dialog open={open} onOpenChange={setOpen}><DialogContent className="interactive-docs-drawer"><DialogHeader><DialogTitle>Learnitive Docs</DialogTitle><DialogDescription>Search documentation and browse pages by category.</DialogDescription></DialogHeader><label className="interactive-docs-filter"><span className="sr-only">Search documentation</span><Search className="size-4"/><input type="search" value={query} onChange={(event)=>setQuery(event.currentTarget.value)} placeholder="Search docs"/></label><div className="interactive-docs-drawer-scroll"><DocsNavigation items={filtered} activeSlug={activeSlug} onNavigate={()=>setOpen(false)}/>{filtered.length===0?<p className="interactive-muted">No documentation matches that search.</p>:null}</div></DialogContent></Dialog></div>;
}

type DocsTocEntry={id:string;label:string;level:2|3};
function docsSlug(value:string) {
  return value.trim().toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/gu,'-').replace(/^-+|-+$/gu,'').slice(0,80)||'section';
}
function prepareDocsHtml(html:string):{html:string;toc:DocsTocEntry[]} {
  if(typeof DOMParser==='undefined') return {html,toc:[]};
  const doc=new DOMParser().parseFromString(`<body>${html}</body>`,'text/html');
  const used=new Set<string>();
  const toc:DocsTocEntry[]=[];
  for(const heading of doc.body.querySelectorAll('h2,h3')) {
    const label=(heading.textContent||'').trim();
    if(!label) continue;
    const base=heading.id||docsSlug(label);
    let id=base; let suffix=2;
    while(used.has(id)){id=`${base}-${suffix}`;suffix+=1;}
    used.add(id); heading.id=id;
    toc.push({id,label,level:heading.tagName==='H3'?3:2});
  }
  return {html:doc.body.innerHTML,toc};
}

async function copyDocsText(text:string) {
  if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(text);return;}
  const textarea=document.createElement('textarea');
  textarea.value=text; textarea.setAttribute('readonly',''); textarea.style.position='fixed'; textarea.style.opacity='0';
  document.body.append(textarea); textarea.select(); document.execCommand('copy'); textarea.remove();
}

function DocsSanitizedHtml({html}:{html:string}) {
  const ref=React.useRef<HTMLDivElement>(null);
  React.useEffect(()=>{
    const cleanups:Array<()=>void>=[];
    for(const block of ref.current?.querySelectorAll('pre')||[]) {
      if(block.querySelector(':scope > .interactive-docs-code-copy')) continue;
      const code=block.querySelector('code')?.textContent||block.textContent||'';
      const button=document.createElement('button');
      button.type='button'; button.className='interactive-docs-code-copy'; button.textContent='Copy'; button.setAttribute('aria-label','Copy code');
      const onClick=()=>{void copyDocsText(code).then(()=>{button.textContent='Copied';window.setTimeout(()=>{button.textContent='Copy';},1200)}).catch(()=>{button.textContent='Copy failed';window.setTimeout(()=>{button.textContent='Copy';},1200)});};
      button.addEventListener('click',onClick); block.append(button); cleanups.push(()=>button.removeEventListener('click',onClick));
    }
    return()=>{for(const cleanup of cleanups)cleanup();};
  },[html]);
  // biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is generated by Learnitive's server-side sanitized Markdown renderer; this component only adds inert heading ids and copy buttons.
  return <div ref={ref} className="interactive-prose interactive-docs-prose" dangerouslySetInnerHTML={{__html:html}}/>;
}

function blogTagHref(tag: string) {
  return `/blog?tag=${encodeURIComponent(tag)}`;
}

function BlogArticle({ item, content }: { item: CmsDetail; content?: React.ReactNode }) {
  const published = item.published_at ? new Date(item.published_at) : null;
  const updated = item.updated_at ? new Date(item.updated_at) : null;
  const dateLabel = published && !Number.isNaN(published.getTime())
    ? published.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
    : '';
  const updatedLabel = updated && !Number.isNaN(updated.getTime()) && (!published || updated.getTime() - published.getTime() > 86_400_000)
    ? updated.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
    : '';
  const authorName = item.author?.trim() || 'Learnitive';
  const authorInitial = authorName.slice(0, 1).toUpperCase();
  const articleUrl = `https://www.learnitive.com/blog/${encodeURIComponent(item.slug)}`;
  const encodedUrl = encodeURIComponent(articleUrl);
  const encodedTitle = encodeURIComponent(item.title);
  const canNativeShare = typeof navigator !== 'undefined' && 'share' in navigator;
  const shareNative = async () => {
    if (!canNativeShare) return;
    try { await navigator.share({ title: item.title, text: item.excerpt || undefined, url: articleUrl }); } catch (error) {
      if (!(error instanceof DOMException && error.name === 'AbortError')) console.warn('[Learnitive] Share failed.');
    }
  };
  const copyLink = async () => {
    try { await navigator.clipboard.writeText(articleUrl); toast.success('Article link copied.'); }
    catch { toast.error('Could not copy the article link.'); }
  };
  return <article className="interactive-blog-article-shell">
    <div className={`interactive-blog-article-hero${item.cover_image_url ? '' : ' interactive-blog-article-hero-placeholder'}`}>
      {item.cover_image_url ? <img src={item.cover_image_url} alt={item.cover_image_alt || `${item.title} — Learnitive`} width={item.cover_image_width || undefined} height={item.cover_image_height || undefined} loading="eager" fetchPriority="high" /> : <FileText className="size-10" aria-hidden />}
    </div>
    <header className="interactive-blog-article-header">
      {dateLabel ? <div className="interactive-blog-written">Written on <time dateTime={published?.toISOString()}>{dateLabel}</time>{updatedLabel ? <> · Updated <time dateTime={updated?.toISOString()}>{updatedLabel}</time></> : null}</div> : null}
      <h1>{item.title}</h1>
      {item.tags?.length ? <nav className="interactive-blog-article-tags" aria-label="Article tags">{item.tags.map((tag) => <a key={tag} href={blogTagHref(tag)} aria-label={`Show blog posts tagged ${tag}`}>{tag}</a>)}</nav> : null}
      <div className="interactive-blog-article-author">
        {item.author_avatar_url ? <img src={item.author_avatar_url} alt="" referrerPolicy="no-referrer" /> : <span aria-hidden>{authorInitial}</span>}
        <strong>{authorName}</strong>
      </div>
      <nav className="interactive-blog-share" aria-label="Share this article">
        {canNativeShare ? <button type="button" onClick={() => void shareNative()}>Share</button> : null}
        <a href={`https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`} target="_blank" rel="noopener noreferrer">X</a>
        <a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`} target="_blank" rel="noopener noreferrer">LinkedIn</a>
        <a href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`} target="_blank" rel="noopener noreferrer">Facebook</a>
        <button type="button" onClick={() => void copyLink()}>Copy link</button>
      </nav>
    </header>
    <div className="interactive-blog-article-content">
      {content || <SanitizedHtml html={item.content_html} className="interactive-prose interactive-blog-article-prose" />}
      <nav className="interactive-prev-next" aria-label="Article pagination">{item.previous ? <a href={`/blog/${item.previous.slug}`}>← {item.previous.title}</a> : <span />}{item.next ? <a href={`/blog/${item.next.slug}`}>{item.next.title} →</a> : null}</nav>
    </div>
  </article>;
}

function CmsDetailPage({kind,slug}:{kind:'blog'|'docs';slug:string}) {
  const { config, auth } = useSaasSession();
  const [item,setItem]=React.useState<CmsDetail>(); const [error,setError]=React.useState(''); const [authOpen,setAuthOpen]=React.useState(false);
  const [authReturnTo,setAuthReturnTo]=React.useState(`/${kind}/${slug}`);
  const [interactiveState,setInteractiveState]=React.useState<import('./saas-api').CmsInteractiveState>();
  const docsLoader=React.useCallback(()=>kind==='docs'?listDocs():Promise.resolve([] as CmsListItem[]),[kind]);
  const docsIndex=useCmsList(docsLoader);
  const structured=Boolean(item?.content_format==='blocks'&&item.content_document);
  const preparedDocs=React.useMemo(()=>kind==='docs'&&item&&!structured?prepareDocsHtml(item.content_html.replaceAll('{{LEARNITIVE_FREE_PLAN_LIMIT}}', String(config?.freePlanProjectLimit ?? 5))):{html:'',toc:item?.content_document?interactiveContentToc(item.content_document):[] as DocsTocEntry[]},[config?.freePlanProjectLimit,kind,item,structured]);
  const requestInteractiveAuth=React.useCallback((blockId?:string)=>{
    if(typeof window==='undefined'){setAuthReturnTo(`/${kind}/${slug}`);setAuthOpen(true);return;}
    const anchor=blockId?`#block-${encodeURIComponent(blockId)}`:window.location.hash;
    if(blockId&&window.location.hash!==anchor)window.history.replaceState(window.history.state,'',`${window.location.pathname}${window.location.search}${anchor}`);
    setAuthReturnTo(safeClientReturnPath(`${window.location.pathname}${window.location.search}${anchor}`,`/${kind}/${slug}`));
    setAuthOpen(true);
  },[kind,slug]);
  const structuredContent=item?.content_document?<CmsInteractiveContent document={item.content_document} kind={kind} slug={item.slug||slug} authenticated={auth.authenticated} onAuthRequired={requestInteractiveAuth} headingIds={kind==='docs'} state={interactiveState}/>:null;
  usePageMetadata({
    title: item?.seo_title || item?.title || (kind === 'blog' ? 'Learnitive Blog' : 'Learnitive Docs'),
    description: item?.seo_description || item?.excerpt || (kind === 'blog' ? 'A published Learnitive article.' : 'Learnitive documentation.'),
    pathname: `/${kind}/${encodeURIComponent(item?.slug||slug)}`,
    noIndex: Boolean(item?.noindex),
    type: kind === 'blog' ? 'article' : 'website',
    publishedTime: kind === 'blog' ? item?.published_at || null : null,
    modifiedTime: kind === 'blog' ? item?.updated_at || item?.published_at || null : null,
    author: kind === 'blog' ? item?.author || null : null,
    tags: kind === 'blog' ? item?.tags || [] : [],
    image: kind === 'blog' ? item?.cover_image_url || null : null,
    imageWidth: kind === 'blog' ? item?.cover_image_width || null : null,
    imageHeight: kind === 'blog' ? item?.cover_image_height || null : null,
    enabled: Boolean(item),
  });
  React.useEffect(()=>{let active=true;setError('');setInteractiveState(undefined); void (kind==='blog'?getBlog(slug):getDoc(slug)).then((value)=>{if(active)setItem(value)}).catch((reason)=>{if(active)setError(reason instanceof Error?reason.message:String(reason))}); return()=>{active=false};},[kind,slug]);
  React.useEffect(()=>{if(!auth.authenticated||!structured||!item?.slug){setInteractiveState(undefined);return;}let active=true;void cmsGetInteractiveState(kind,item.slug).then(state=>{if(active)setInteractiveState(state)}).catch(()=>{if(active)setInteractiveState(undefined)});return()=>{active=false};},[auth.authenticated,item?.slug,kind,structured]);
  if(error) return <MarketingPageHeading eyebrow={kind==='blog'?'Blog':'Docs'} title="Page not found" copy={error}/>;
  if(!item) return <RouteLoadingSkeleton />;
  if(kind==='blog') return <><section className="interactive-page-container interactive-blog-detail-page"><BlogArticle item={item} content={structured?structuredContent:undefined}/></section><Dialog open={authOpen} onOpenChange={setAuthOpen}><DialogContent className="interactive-auth-dialog p-0 sm:max-w-md"><DialogHeader className="sr-only"><DialogTitle>Sign in</DialogTitle><DialogDescription>Sign in to use this interactive article.</DialogDescription></DialogHeader><InteractiveAuthCard config={config} returnTo={authReturnTo} description="Sign in to use this interactive content. You will return to this exact block after authentication." onContinueWithoutAccount={()=>setAuthOpen(false)}/></DialogContent></Dialog></>;
  return <><div className="interactive-page-container interactive-docs-mobile-bar"><DocsMobileNavigation items={docsIndex.items} activeSlug={slug}/></div><section className="interactive-page-container interactive-docs-layout"><aside className="interactive-docs-sidebar interactive-docs-sidebar-desktop">{docsIndex.error?<ErrorNotice>{docsIndex.error}</ErrorNotice>:null}{docsIndex.loading?<div className="interactive-docs-nav-skeleton" aria-hidden="true"><i/><i/><i/><i/></div>:<DocsNavigation items={docsIndex.items} activeSlug={slug}/>}</aside><article className="interactive-prose-page interactive-docs-article"><header><nav className="interactive-docs-breadcrumb" aria-label="Breadcrumb"><a href="/docs">Docs</a><span aria-hidden>/</span><span>{item.category||'Documentation'}</span></nav><h1 className="interactive-display">{item.title}</h1>{item.excerpt?<p className="interactive-docs-excerpt">{item.excerpt}</p>:null}{item.tags?.length?<div className="interactive-tag-row">{item.tags.map((tag)=><span key={tag}>{tag}</span>)}</div>:null}</header>{structured?structuredContent:<DocsSanitizedHtml html={preparedDocs.html}/>}<nav className="interactive-prev-next" aria-label="Documentation pagination">{item.previous?<a href={`/docs/${item.previous.slug}`}>← {item.previous.title}</a>:<span/>}{item.next?<a href={`/docs/${item.next.slug}`}>{item.next.title} →</a>:null}</nav></article><aside className="interactive-docs-toc" aria-label="On this page"><strong>On this page</strong>{preparedDocs.toc.length?<nav>{preparedDocs.toc.map((entry)=><a key={entry.id} href={`#${entry.id}`} data-level={entry.level}>{entry.label}</a>)}</nav>:<span className="interactive-muted">This page has no section headings.</span>}</aside></section><Dialog open={authOpen} onOpenChange={setAuthOpen}><DialogContent className="interactive-auth-dialog p-0 sm:max-w-md"><DialogHeader className="sr-only"><DialogTitle>Sign in</DialogTitle><DialogDescription>Sign in to use this interactive documentation.</DialogDescription></DialogHeader><InteractiveAuthCard config={config} returnTo={authReturnTo} description="Sign in to use this interactive documentation. You will return to this exact block after authentication." onContinueWithoutAccount={()=>setAuthOpen(false)}/></DialogContent></Dialog></>;
}

function DocsPage() {
  const loader=React.useCallback(()=>listDocs(),[]); const {items,loading,error}=useCmsList(loader);
  const [query,setQuery]=React.useState('');
  const normalized=query.trim().toLowerCase();
  const filtered=normalized?items.filter((item)=>[item.title,item.category,item.excerpt,...(item.tags||[])].filter(Boolean).some((value)=>String(value).toLowerCase().includes(normalized))):items;
  const grouped=groupDocs(filtered);
  const quickLinks = [
    ['Getting Started','/docs/getting-started','Create your first Markdown or LaTeX project and learn the core writing workflow.'],
    ['Creating Projects','/docs/creating-projects','Choose a writing format, start from a template and organise work into portable projects.'],
    ['Project Storage','/docs/project-storage','Understand local-first storage, linked folders, browser projects and backups.'],
    ['Editor Overview','/docs/editor-overview','Use the document editor, file tree, tabs, outline and preview tools together.'],
    ['AI Writing','/docs/ai-overview','Use optional local AI or your own provider key for writing and research assistance.'],
    ['BYOK AI','/docs/byok-ai','Connect your own supported AI provider without changing how project files are stored.'],
    ['Paper Memory','/docs/paper-memory','Bring research papers into project context for grounded writing and review.'],
    ['Templates','/docs/templates-guide','Start from curated Markdown and LaTeX writing templates.'],
    ['Learn','/docs/learn','Follow writing, research and learning material without leaving the workspace.'],
    ['LaTeX & Scientific','/docs/latex-scientific','Write technical documents with equations, citations, bibliography and PDF output.'],
    ['Research Workflows','/docs/research-workflows','Organise sources, citations, deep research and long-form academic projects.'],
    ['Plans & Features','/docs/plans-and-features','See what is included in Free and the $29 Pro Lifetime licence.'],
  ] as const;
  return <>
    <MarketingPageHeading eyebrow="Docs" title="Learnitive Docs" copy="Guides for Markdown and LaTeX writing, research, citations, local-first project storage, optional AI and Learnitive workflows."/>
    <section className="interactive-page-container interactive-docs-start" aria-labelledby="docs-start-title"><div className="interactive-section-heading interactive-centered-heading"><span className="interactive-eyebrow">Start</span><h2 id="docs-start-title">Popular Guides</h2><p>Start with the workflow you need now, then browse the full documentation by category.</p></div><div className="interactive-docs-start-grid">{quickLinks.map(([title,href,copy])=><a key={href} href={href}><strong>{title}</strong><span>{copy}</span><span className="interactive-text-link">Read guide <ChevronRight className="size-4" aria-hidden="true" /></span></a>)}</div></section>
    <div className="interactive-page-container interactive-docs-mobile-bar"><DocsMobileNavigation items={items}/></div>
    <section className="interactive-page-container interactive-docs-index"><aside className="interactive-docs-sidebar interactive-docs-sidebar-desktop"><label className="interactive-docs-filter"><span className="sr-only">Filter documentation</span><Search className="size-4"/><input type="search" value={query} onChange={(event)=>setQuery(event.currentTarget.value)} placeholder="Filter docs"/></label>{!loading?<DocsNavigation items={items}/>:null}</aside><div className="interactive-docs-results">{error?<ErrorNotice>{error}</ErrorNotice>:null}{loading?<LoadingCard/>:grouped.map(([category,docs])=><section key={category}><h2>{category}</h2>{docs.map((doc)=><a key={doc.slug} href={`/docs/${doc.slug}`}><span><strong>{doc.title}</strong>{doc.excerpt?<small>{doc.excerpt}</small>:null}</span><ChevronRight className="size-4"/></a>)}</section>)}{!loading&&!filtered.length?<EmptyCms label={items.length?'No documentation matches that filter.':'No published documentation pages yet.'}/>:null}</div></section>
  </>;
}
function EmptyCms({label}:{label:string}) { return <div className="interactive-empty-state"><BookOpen className="size-6"/><h2>{label}</h2></div>; }

function ContactPage() {
  const {auth,config}=useSaasSession(); const [state,setState]=React.useState({name:auth.authenticated?auth.user.displayName:'',email:auth.authenticated?auth.user.email:'',subject:'',message:'',website:''}); const [busy,setBusy]=React.useState(false); const [done,setDone]=React.useState(false);
  React.useEffect(()=>{if(auth.authenticated)setState((s)=>({...s,name:s.name||auth.user.displayName,email:s.email||auth.user.email}))},[auth]);
  const submit=async(event:React.FormEvent)=>{event.preventDefault(); setBusy(true); try{await submitContact(state);setDone(true);toast.success('Support request sent.')}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setBusy(false)}};
  return <><MarketingPageHeading eyebrow="Contact" title="Talk to Learnitive." copy="Send a support request. The message is stored as a support ticket and can notify the configured support mailbox."/><section className="interactive-page-container interactive-form-wrap">{done?<div className="interactive-success"><Check className="size-5"/><strong>Thanks. Your support ticket was created.</strong></div>:<form onSubmit={(event)=>void submit(event)}><div><Label htmlFor="contact-name">Name</Label><Input id="contact-name" required maxLength={160} value={state.name} onChange={(e)=>setState({...state,name:e.currentTarget.value})}/></div><div><Label htmlFor="contact-email">Email</Label><Input id="contact-email" type="email" required value={state.email} onChange={(e)=>setState({...state,email:e.currentTarget.value})}/></div><div className="interactive-honeypot" aria-hidden><Label htmlFor="contact-website">Website</Label><Input id="contact-website" tabIndex={-1} autoComplete="off" value={state.website} onChange={(e)=>setState({...state,website:e.currentTarget.value})}/></div><div><Label htmlFor="contact-subject">Subject</Label><Input id="contact-subject" required minLength={3} maxLength={240} value={state.subject} onChange={(e)=>setState({...state,subject:e.currentTarget.value})}/></div><div><Label htmlFor="contact-message">Message</Label><textarea id="contact-message" className="interactive-textarea" required minLength={10} maxLength={12000} value={state.message} onChange={(e)=>setState({...state,message:e.currentTarget.value})}/></div><Button disabled={busy||!config?.contactEnabled} type="submit">{busy?'Sending…':'Send message'}</Button>{!config?.contactEnabled?<p className="interactive-muted">Contact delivery is not configured on this deployment.</p>:null}</form>}</section></>;
}

function LegalPage({slug}:{slug:'privacy'|'terms'}) {
  const [page,setPage]=React.useState<LegalPageRecord>();
  const [error,setError]=React.useState('');
  React.useEffect(()=>{let active=true;void getLegalPage(slug).then((next)=>{if(active)setPage(next)}).catch((reason)=>{if(active)setError(reason instanceof Error?reason.message:String(reason))});return()=>{active=false}},[slug]);
  if(error)return <article className="interactive-page-container interactive-simple-page"><ErrorNotice>{error}</ErrorNotice></article>;
  if(!page)return <article className="interactive-page-container interactive-simple-page"><LoadingCard/></article>;
  return <article className="interactive-page-container interactive-simple-page"><h1 className="interactive-display">{page.title}</h1><DocsSanitizedHtml html={page.content_html||''}/></article>;
}
const PUBLIC_TOOLS = [
  { slug:'bibtex-validator', title:'BibTeX Validator', summary:'Check BibTeX entries locally for malformed entry headers, unbalanced braces and duplicate citation keys.' },
  { slug:'citation-key-checker', title:'Citation Key Checker', summary:'Find duplicate and non-portable citation keys before they break a large bibliography.' },
  { slug:'latex-table-generator', title:'LaTeX Table Generator', summary:'Turn tab-, comma- or pipe-separated rows into a clean tabular environment in your browser.' },
  { slug:'latex-error-decoder', title:'LaTeX Error Decoder', summary:'Paste a LaTeX compiler error and get a focused explanation of common causes and next checks.' },
] as const;

function ToolsPage() {
  return <><MarketingPageHeading eyebrow="Free tools" title="Small LaTeX tools that solve a complete task." copy="These utilities run in your browser and are designed to help before you open or continue a full Learnitive project."/><section className="interactive-page-container interactive-card-grid">{PUBLIC_TOOLS.map((tool)=><a className="interactive-marketing-card interactive-use-case-card" href={`/tools/${tool.slug}`} key={tool.slug}><FileCode2 className="size-5"/><h2>{tool.title}</h2><p>{tool.summary}</p><span className="interactive-text-link">Open tool <ChevronRight className="size-4"/></span></a>)}</section></>;
}

function bibtexKeys(input:string): string[] { return [...input.matchAll(/@[A-Za-z]+\s*\{\s*([^,\s}]+)/gu)].flatMap((match)=>match[1] ? [match[1]] : []); }
function BibtexValidatorTool({ keysOnly=false }:{keysOnly?:boolean}) {
  const [input,setInput]=React.useState('');
  const keys=React.useMemo(()=>bibtexKeys(input),[input]);
  const results=React.useMemo(()=>{
    const issues:string[]=[];
    const counts=new Map<string,number>();
    for(const key of keys){counts.set(key,(counts.get(key)||0)+1);if(!/^[A-Za-z0-9:._+-]+$/u.test(key))issues.push(`Citation key “${key}” contains characters that are best avoided in portable BibTeX workflows.`)}
    for(const [key,count] of counts)if(count>1)issues.push(`Duplicate citation key “${key}” appears ${count} times.`);
    if(!keysOnly){let depth=0;let escaped=false;for(const ch of input){if(escaped){escaped=false;continue}if(ch==='\\'){escaped=true;continue}if(ch==='{')depth++;if(ch==='}')depth--;if(depth<0){issues.push('A closing brace appears before a matching opening brace.');break}}if(depth>0)issues.push(`${depth} opening brace${depth===1?' is':'s are'} not closed.`);if(input.trim()&&!keys.length)issues.push('No BibTeX entry header such as @article{key, ...} was detected.');}
    return [...new Set(issues)];
  },[input,keys,keysOnly]);
  return <ToolShell title={keysOnly?'Citation Key Checker':'BibTeX Validator'} copy={keysOnly?PUBLIC_TOOLS[1].summary:PUBLIC_TOOLS[0].summary}><Label htmlFor="bibtex-tool-input">Paste BibTeX</Label><textarea id="bibtex-tool-input" className="interactive-tool-textarea" rows={14} value={input} onChange={(e)=>setInput(e.currentTarget.value)} placeholder="@article{smith2026, ...}"/><div className="interactive-tool-result"><strong>{input.trim()?results.length?`${results.length} issue${results.length===1?'':'s'} found`:`No ${keysOnly?'citation-key':'basic structural'} issues found`:'Paste BibTeX to begin.'}</strong>{results.length?<ul>{results.map((issue)=><li key={issue}>{issue}</li>)}</ul>:null}{keys.length?<p>Detected {keys.length} entr{keys.length===1?'y':'ies'}.</p>:null}</div></ToolShell>;
}

function LatexTableGeneratorTool(){const[input,setInput]=React.useState('Name\tValue\nAlpha\t10\nBeta\t12');const[delimiter,setDelimiter]=React.useState<'tab'|'comma'|'pipe'>('tab');const output=React.useMemo(()=>{const sep=delimiter==='tab'?'\t':delimiter==='comma'?',':'|';const rows=input.split(/\r?\n/u).filter(Boolean).map((row)=>row.split(sep).map((cell)=>cell.trim()));const cols=Math.max(1,...rows.map((row)=>row.length));const esc=(v:string)=>v.replace(/([%&#_$])/gu,'\\$1');return `\\begin{tabular}{${'l'.repeat(cols)}}\n${rows.map((row)=>row.map(esc).join(' & ')+' \\\\').join('\n')}\n\\end{tabular}`},[input,delimiter]);return <ToolShell title="LaTeX Table Generator" copy={PUBLIC_TOOLS[2].summary}><div className="interactive-tool-inline"><Label>Input separator</Label><select value={delimiter} onChange={(e)=>setDelimiter(e.currentTarget.value as typeof delimiter)}><option value="tab">Tab</option><option value="comma">Comma</option><option value="pipe">Pipe</option></select></div><textarea className="interactive-tool-textarea" rows={9} value={input} onChange={(e)=>setInput(e.currentTarget.value)}/><Label>Generated LaTeX</Label><pre className="interactive-learn-code"><code>{output}</code></pre><Button variant="outline" onClick={()=>void navigator.clipboard?.writeText(output)}>Copy LaTeX</Button></ToolShell>}

const LATEX_ERROR_RULES:[RegExp,string,string][]=[
  [/Undefined control sequence/iu,'Undefined control sequence','Check the command spelling first, then whether the package that defines it is loaded before use. The log line immediately following the error usually shows the unknown command.'],
  [/File [`']?([^`'\s]+)[`']? not found/iu,'Missing file or package','Confirm the file is inside the project with the same case-sensitive name. For .sty files, check whether the required package exists in the selected TeX environment.'],
  [/Missing \\begin\{document\}/iu,'Missing document start','Check that the root file contains \\begin{document} and that Learnitive is compiling the intended root file rather than a chapter fragment.'],
  [/Runaway argument|Paragraph ended before/iu,'Unclosed argument or brace','Look just before this error for an opening brace or command argument that never closes. The true mistake is often several lines earlier.'],
  [/Environment ([^ ]+) undefined/iu,'Unknown environment','Check the environment name and load the package that defines it. Also verify that \\begin and \\end names match exactly.'],
  [/Extra alignment tab has been changed to \\cr/iu,'Too many table alignment cells','A table row contains more & separators than the column specification allows. Compare the failing row with the tabular column count.'],
  [/There were undefined references|Reference .* undefined/iu,'Undefined reference','Compile enough passes for references to settle, then confirm every \\ref key has a matching \\label.'],
  [/Citation .* undefined|There were undefined citations/iu,'Undefined citation','Confirm the citation key exists in the bibliography, the bibliography file is included, and the selected BibTeX/Biber workflow has run successfully.'],
];
function LatexErrorDecoderTool(){const[input,setInput]=React.useState('');const match=React.useMemo(()=>LATEX_ERROR_RULES.find(([pattern])=>pattern.test(input)),[input]);return <ToolShell title="LaTeX Error Decoder" copy={PUBLIC_TOOLS[3].summary}><Label htmlFor="latex-error-input">Compiler error</Label><textarea id="latex-error-input" className="interactive-tool-textarea" rows={9} value={input} onChange={(e)=>setInput(e.currentTarget.value)} placeholder="Paste the first meaningful LaTeX error here…"/><div className="interactive-tool-result"><strong>{input.trim()?match?.[1]||'No common pattern matched yet':'Paste an error to begin.'}</strong>{input.trim()?<p>{match?.[2]||'Start with the first fatal error in the log, inspect the source line it names, and check for unmatched braces, environments, missing files or packages before changing unrelated code.'}</p>:null}</div></ToolShell>}
function ToolShell({title,copy,children}:{title:string;copy:string;children:React.ReactNode}){return <article className="interactive-page-container interactive-tool-page"><header><a className="interactive-text-link" href="/tools">← All tools</a><span className="interactive-eyebrow">Free browser tool</span><h1 className="interactive-display">{title}</h1><p>{copy}</p></header><section className="interactive-tool-panel">{children}</section><section className="interactive-tool-cta"><h2>Continue in Learnitive</h2><p>Open a local first LaTeX project when you are ready to apply the result to a manuscript.</p><a className="interactive-button interactive-button-primary" href="/projects">Open Learnitive Editor</a></section></article>}
function ToolPage({slug}:{slug:string}){if(slug==='bibtex-validator')return <BibtexValidatorTool/>;if(slug==='citation-key-checker')return <BibtexValidatorTool keysOnly/>;if(slug==='latex-table-generator')return <LatexTableGeneratorTool/>;if(slug==='latex-error-decoder')return <LatexErrorDecoderTool/>;return <NotFoundPage/>}

const ABOUT_PLATFORM_SLIDES = [
  {
    eyebrow: 'Markdown · Visual Writing',
    title: 'A Writer First',
    copy: 'Learnitive keeps writing focused on ordinary project files. Use a visual document workflow when it helps, switch to Markdown source when you want direct control, and keep the result portable.',
    image: '/learnitive-team3.webp',
    imageAlt: 'Learnitive team in the Learnitive workspace',
    actions: [{ label: 'Explore writing features', href: '/features' }],
  },
  {
    eyebrow: 'LaTeX · Research · PDF',
    title: 'Academic Writing Without Lock-In',
    copy: 'LaTeX remains first-class for research papers, theses and technical documents, with citations, bibliography workflows, research context and PDF output around source you can keep using outside Learnitive.',
    image: '/learnitive-office3.webp',
    imageAlt: 'Learnitive office workspace for learning, research and writing',
    actions: [
      { label: 'Browse academic templates', href: '/templates' },
      { label: 'Explore research tools', href: '/tools' },
    ],
  },
] as const;

function AboutPlatformCarousel() {
  const [activeSlide, setActiveSlide] = React.useState(0);
  const [autoPaused, setAutoPaused] = React.useState(false);
  const [reduceMotion, setReduceMotion] = React.useState(false);

  React.useEffect(() => {
    const media = window.matchMedia('(prefers-reduced-motion: reduce)');
    const update = () => setReduceMotion(media.matches);
    update();
    media.addEventListener?.('change', update);
    return () => media.removeEventListener?.('change', update);
  }, []);

  React.useEffect(() => {
    if (autoPaused || reduceMotion) return undefined;
    const timer = window.setInterval(() => {
      setActiveSlide((current) => current >= ABOUT_PLATFORM_SLIDES.length - 1 ? 0 : current + 1);
    }, 8000);
    return () => window.clearInterval(timer);
  }, [autoPaused, reduceMotion]);

  const setSlide = (next: number) => setActiveSlide(Math.max(0, Math.min(ABOUT_PLATFORM_SLIDES.length - 1, next)));
  return (
    <section className="interactive-page-container interactive-about-carousel-section" aria-label="Learnitive writing and research highlights">
      <div
        className="interactive-about-carousel"
        role="region"
        aria-roledescription="carousel"
        aria-label="Learnitive highlights"
        onPointerEnter={() => setAutoPaused(true)}
        onPointerLeave={() => setAutoPaused(false)}
        onFocus={() => setAutoPaused(true)}
        onBlur={(event) => {
          if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setAutoPaused(false);
        }}
      >
        <div className="interactive-about-carousel-viewport">
          <div className="interactive-about-carousel-track" style={{ transform: `translateX(-${activeSlide * 100}%)` }}>
            {ABOUT_PLATFORM_SLIDES.map((slide, index) => (
              <article className="interactive-about-carousel-slide" key={slide.title} aria-hidden={activeSlide !== index}>
                <div className="interactive-about-carousel-copy">
                  <span className="interactive-about-carousel-kicker">{slide.eyebrow}</span>
                  <h2>{slide.title}</h2>
                  <p>{slide.copy}</p>
                  <div className="interactive-about-carousel-actions">
                    {slide.actions.map((action, actionIndex) => (
                      <a
                        className={`interactive-button ${actionIndex === 0 ? 'interactive-button-primary' : 'interactive-button-secondary'}`}
                        href={action.href}
                        key={action.href}
                        tabIndex={activeSlide === index ? undefined : -1}
                      >
                        {action.label}{actionIndex === 0 ? <ArrowRight className="size-4" aria-hidden="true" /> : null}
                      </a>
                    ))}
                  </div>
                </div>
                <div className="interactive-about-carousel-media">
                  <img src={slide.image} alt={slide.imageAlt} loading={index === 0 ? 'eager' : 'lazy'} decoding="async" />
                </div>
              </article>
            ))}
          </div>
        </div>
        <button
          type="button"
          className="interactive-about-carousel-arrow interactive-about-carousel-arrow-prev"
          onClick={() => setSlide(activeSlide - 1)}
          disabled={activeSlide === 0}
          aria-label="Previous highlight"
        >
          <ChevronLeft className="size-5" aria-hidden="true" />
        </button>
        <button
          type="button"
          className="interactive-about-carousel-arrow interactive-about-carousel-arrow-next"
          onClick={() => setSlide(activeSlide + 1)}
          disabled={activeSlide === ABOUT_PLATFORM_SLIDES.length - 1}
          aria-label="Next highlight"
        >
          <ChevronRight className="size-5" aria-hidden="true" />
        </button>
        <div className="interactive-about-carousel-status" aria-label={`Slide ${activeSlide + 1} of ${ABOUT_PLATFORM_SLIDES.length}`}>
          {ABOUT_PLATFORM_SLIDES.map((slide, index) => (
            <button
              type="button"
              key={slide.title}
              aria-label={`Show ${slide.title}`}
              aria-current={activeSlide === index ? 'true' : undefined}
              onClick={() => setSlide(index)}
            />
          ))}
        </div>
      </div>
    </section>
  );
}


function FaqPage() {
  const questions = [
    ['What is Learnitive?', 'Learnitive is a local-first writing, research and learning workspace. Markdown and LaTeX are first-class, with citations, PDF publishing, research tools and optional AI assistance around portable project files.'],
    ['Where are my projects stored?', 'Projects can live in browser storage and can optionally be synchronized to a Projects Folder through supported browser filesystem access. Learnitive is designed so ordinary project files remain understandable and portable.'],
    ['Does Learnitive support Markdown and visual editing?', 'Yes. Markdown is the default new-project type and can be edited as source or through the supported visual writing workflow.'],
    ['Does Learnitive support LaTeX?', 'Yes. LaTeX projects support source and visual workflows, mathematics, bibliography files, citations and PDF-oriented academic writing.'],
    ['Can I use AI without sending my projects to one fixed provider?', 'Yes. Learnitive supports explicit AI choices including local models where supported and BYOK provider configuration for Pro users. AI is optional around the underlying project files.'],
    ['Can I still create code projects?', 'Yes. New Project keeps Markdown and LaTeX prominent. Choose Code to expand the retained language and framework catalogue when you need programming tools.'],
    ['What is Learnitive Pro Lifetime?', 'The launch plan is a one-time $29 Pro Lifetime licence, with the exact included features shown on the Pricing page.'],
  ] as const;
  return <article className="interactive-page-container interactive-faq-page"><header className="interactive-page-heading"><span className="interactive-eyebrow">FAQ</span><h1 className="interactive-display">Learnitive questions, answered.</h1><p>Writing, research, local-first projects, AI, LaTeX and optional Code.</p></header><div className="interactive-faq-list">{questions.map(([question, answer]) => <section key={question}><h2>{question}</h2><p>{answer}</p></section>)}</div><p><a className="interactive-button interactive-button-secondary" href="/docs">Read the documentation</a></p></article>;
}

function AboutPage() {
  const pillars = [
    { icon: FileText, title: 'Markdown Writer', copy: 'Create notes, articles, documentation, books and long-form work in portable Markdown with visual editing when you want it.' },
    { icon: FileCode2, title: 'LaTeX and PDF', copy: 'Write papers, theses and technical documents with LaTeX, mathematics, bibliography files and publication-ready PDF output.' },
    { icon: BookOpen, title: 'Research', copy: 'Keep citations, references, Paper Memory and research context close to the manuscript instead of scattering them across unrelated tools.' },
    { icon: Sparkles, title: 'AI Assistance', copy: 'Draft, rewrite, explain and summarise with explicit Local AI, Ollama or BYOK provider choices. Learnitive never needs to become the owner of your source files.' },
    { icon: FolderOpen, title: 'Local First', copy: 'Work from browser storage and optionally synchronize projects to a Projects Folder so ordinary files remain under your control.' },
    { icon: Users, title: 'Learning and Collaboration', copy: 'Use project-based learning and authenticated collaboration around the same documents, with Tutor and Whiteboard planned as separate Learnitive applications.' },
  ] as const;
  return <article className="interactive-about-page">
    <header className="interactive-page-container interactive-about-hero interactive-about-hero-grid">
      <div className="interactive-about-hero-copy"><span className="interactive-eyebrow">About Learnitive</span><h1 className="interactive-display">Learnitive</h1><p>Learnitive is a local-first writing, research and learning workspace built around Markdown, visual writing, LaTeX, citations, PDF publishing and optional AI assistance.</p><div className="interactive-hero-actions"><a className="interactive-button interactive-button-primary interactive-button-lg" href="/projects">Start writing <ArrowRight className="size-4" aria-hidden="true" /></a><a className="interactive-button interactive-button-secondary interactive-button-lg" href="/docs">View docs</a></div></div>
      <figure className="interactive-about-office"><img src="/learnitive-office.webp" alt="Learnitive learning, research and writing workspace"/><figcaption>Write, research and publish from projects that remain portable and understandable.</figcaption></figure>
    </header>

    <section className="interactive-page-container interactive-about-section interactive-about-mission" aria-labelledby="about-mission-title"><span className="interactive-eyebrow">Learnitive</span><h2 id="about-mission-title">Why Learnitive</h2><p>Writing tools should help you think without trapping the result. Learnitive keeps Markdown and LaTeX source at the centre, adds visual editing and research tools around those files, and lets you choose when AI or collaboration adds value.</p><p>Local-first project storage is a product principle rather than an export afterthought. Your documents stay portable, AI provider choice stays explicit, and online features remain optional layers around the writing project.</p></section>

    <section className="interactive-page-container interactive-about-section" aria-labelledby="about-pillars-title"><div className="interactive-about-section-heading"><span className="interactive-eyebrow">Workspace</span><h2 id="about-pillars-title">A workspace for writing and research</h2></div><div className="interactive-about-values interactive-about-pillars">{pillars.map((item)=>{const Icon=item.icon;return <article key={item.title}><span className="interactive-about-value-icon"><Icon className="size-5" aria-hidden="true"/></span><h3>{item.title}</h3><p>{item.copy}</p></article>;})}</div></section>

    <section className="interactive-page-container interactive-about-section" aria-labelledby="about-how-title"><div className="interactive-about-section-heading"><span className="interactive-eyebrow">Workflow</span><h2 id="about-how-title">How it works</h2></div><ol className="interactive-about-steps"><li><span>01</span><div><h3>Start a document project</h3><p>Create Markdown for general writing or LaTeX for academic and technical publishing, or apply a writing-focused template.</p></div></li><li><span>02</span><div><h3>Write and research</h3><p>Use visual or source editing, citations, bibliography tools, research context and optional AI assistance around the same project files.</p></div></li><li><span>03</span><div><h3>Keep control of the result</h3><p>Use browser storage and an optional Projects Folder, then export or publish only when you choose.</p></div></li></ol></section>

    <AboutPlatformCarousel />

    <section className="interactive-page-container interactive-about-contact" aria-labelledby="about-contact-title"><span className="interactive-eyebrow">Start</span><h2 id="about-contact-title">Start Writing</h2><p>Create a Markdown or LaTeX project, keep the source portable, and bring in AI or research tools only when they help.</p><div className="interactive-hero-actions"><a className="interactive-button interactive-button-primary" href="/projects">Open Learnitive <ArrowRight className="size-4" aria-hidden="true" /></a><a className="interactive-button interactive-button-secondary" href="/docs">Read the docs</a></div></section>
  </article>;
}

function licensePlanLabel(entitlement: SaasEntitlement) {
  if (entitlement.blocked) return 'Blocked';
  if (entitlement.expired) return 'Free · Monthly expired';
  if (!entitlement.pro) return 'Free';
  if (entitlement.license?.type === 'monthly') return 'Pro Monthly';
  if (entitlement.license?.type === 'lifetime') return 'Pro Lifetime';
  return 'Pro';
}

function licenseErrorCopy(reason: unknown) {
  if (reason instanceof SaasApiError) {
    if (reason.code === 'authentication_required') return 'Sign in before linking a Learnitive Pro license.';
    if (reason.code === 'license_expired') return 'This 1-month Learnitive Pro license has expired.';
    if (reason.code === 'license_blocked') return 'This Learnitive Pro license is blocked.';
    if (reason.code === 'license_already_linked') return 'This Learnitive Pro license is already linked to another account.';
    if (reason.code === 'license_code_used') return 'This license code has already been used.';
    if (reason.code === 'license_lifetime_already_active') return 'This account already has Pro Lifetime. The code was not consumed.';
    if (reason.code === 'rate_limited') return 'Too many verification attempts. Please wait and try again.';
    if (reason.code === 'license_service_unavailable') return 'License verification is temporarily unavailable. Try again shortly.';
    if (reason.code === 'invalid_license') return "We couldn't verify this Learnitive Pro license key.";
  }
  return "We couldn't verify this Learnitive Pro license right now. Try again.";
}

function LicenseLinkCard({ inputId = 'license-key', showCurrentPlan = false, locked = false }: { inputId?: string; showCurrentPlan?: boolean; locked?: boolean }) {
  const { auth, refresh } = useSaasSession();
  const [license, setLicense] = React.useState('');
  const [licenseBusy, setLicenseBusy] = React.useState(false);
  const [licenseError, setLicenseError] = React.useState('');
  const [linkedAccount, setLinkedAccount] = React.useState<SaasEntitlement>();
  const entitlement = linkedAccount ?? (auth.authenticated ? auth.entitlement : undefined);
  const activate = async () => {
    const key = license.trim();
    if (!key || licenseBusy || locked || !auth.authenticated) return;
    setLicenseBusy(true); setLicenseError(''); setLinkedAccount(undefined);
    try {
      const result = await verifyAccountLicense(key);
      setLicense('');
      setLinkedAccount(result.account);
      await refresh();
      toast.success(result.account.license?.type === 'monthly' ? 'Pro Monthly activated.' : result.account.license?.type === 'lifetime' ? 'Pro Lifetime activated.' : 'Learnitive Pro activated.');
    } catch (reason) {
      console.warn('[Learnitive] Pro license verification failed.', reason);
      setLicenseError(licenseErrorCopy(reason));
    } finally { setLicenseBusy(false); }
  };
  const linkedType = linkedAccount?.license?.type;
  const linkedExpiry = linkedAccount?.license?.expiresAt;
  return <section className="interactive-settings-card interactive-license-link-card">
    <div className="interactive-license-link-heading"><span className="interactive-eyebrow">Learnitive Pro</span><h2>Link a Pro license</h2><p>Enter your Learnitive Pro license code to link it to this account. Lifetime, 1-month Payhip and eligible 10-character activation codes are verified securely on the server.</p></div>
    {showCurrentPlan && entitlement ? <div className="interactive-license-current-plan"><span>Current plan</span><strong>{licensePlanLabel(entitlement)}</strong>{entitlement.license?.type === 'monthly' && entitlement.license.expiresAt && !entitlement.expired ? <small>Active until {new Date(entitlement.license.expiresAt).toLocaleDateString()}</small> : null}</div> : null}
    {linkedAccount ? <div className="interactive-license-success" role="status"><Check className="size-4" /><span>{linkedType === 'monthly' ? <>Pro Monthly activated. Pro is active until {linkedExpiry ? new Date(linkedExpiry).toLocaleDateString() : 'the license expiry date'}.</> : linkedType === 'lifetime' ? <>Pro Lifetime activated. Unlimited projects, clean PDF export and Priority support are now enabled.</> : <>Learnitive Pro activated for this account.</>}</span></div> : null}
    <Label htmlFor={inputId}>License key</Label>
    <Input id={inputId} type="password" value={license} disabled={locked || licenseBusy} onChange={(e) => { setLicense(e.currentTarget.value); setLicenseError(''); setLinkedAccount(undefined); }} placeholder="License key" autoComplete="off" spellCheck={false} />
    {licenseError ? <p className="interactive-license-error" role="alert">{licenseError}</p> : null}
    <div><Button disabled={locked || licenseBusy || !license.trim()} onClick={() => void activate()}>{licenseBusy ? 'Linking…' : 'Link license'}</Button></div>
  </section>;
}

function ApplyLicensePage() {
  const { auth, loading } = useSaasSession();
  return <section className="interactive-page-container interactive-apply-page">
    <header className="interactive-apply-heading"><span className="interactive-eyebrow">Learnitive Pro</span><h1 className="interactive-display">Apply your license.</h1><p>Use one secure form for a Pro Lifetime, 1-month Payhip or eligible 10-character Learnitive activation code. Learnitive verifies the license on the server and links it to your account.</p></header>
    {loading ? <div className="interactive-apply-loading"><LoadingCard /></div> : auth.authenticated ? <div className="interactive-apply-card-wrap"><LicenseLinkCard inputId="apply-license-key" showCurrentPlan /></div> : <div className="interactive-apply-license-gate">
      <div className="interactive-apply-license-blurred" aria-hidden="true"><LicenseLinkCard inputId="apply-license-preview-key" locked /></div>
      <div className="interactive-apply-login-overlay"><span className="interactive-eyebrow">Account required</span><h2>Sign in to apply your license</h2><p>License codes are linked to a Learnitive account so Pro status can follow you securely.</p><a className="interactive-button interactive-apply-login-button" href={`/login?returnTo=${encodeURIComponent('/apply')}`}><CircleUserRound className="size-4" aria-hidden="true" /> Login</a></div>
    </div>}
    <p className="interactive-apply-note">The license key is sent only to Learnitive's authenticated verification endpoint and is never placed in the URL.</p>
  </section>;
}

function SimplePage({title,body}:{title:string;body:string}){return <article className="interactive-page-container interactive-simple-page"><h1 className="interactive-display">{title}</h1><p>{body}</p></article>}
function NotFoundPage(){return <SimplePage title="Page not found" body="The requested Learnitive page does not exist."/>}
function ErrorNotice({children}:{children:React.ReactNode}){return <div className="interactive-error" role="alert">{children}</div>}

function SanitizedHtml({html,className="interactive-prose"}:{html:string;className?:string}) {
  // biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is generated by Learnitive's server-side sanitized Markdown renderer before reaching this component.
  return <div className={className} dangerouslySetInnerHTML={{__html:html}} />;
}

// ---------------------------------------------------------------------------
// Projects: local workspace authority + optional filesystem recovery
// ---------------------------------------------------------------------------
// Legacy explicit server-side shares can still reference historical project rows.
// Normal local projects never receive or require this metadata.
function workspaceCloudId(workspace: WorkspaceInfo): string | undefined {
  return typeof workspace.metadata?.saasProjectId === 'string' ? workspace.metadata.saasProjectId : undefined;
}

function isTemporaryLearnWorkspace(workspace: WorkspaceInfo): boolean { return workspace.metadata?.learnTemporary === true; }

function workspaceProjectSlug(workspace: WorkspaceInfo): string {
  const value = workspace.metadata?.projectSlug;
  return typeof value === 'string' && value.trim() ? value.trim() : workspace.name;
}

function workspaceProjectDisplayName(workspace: WorkspaceInfo): string {
  const value = workspace.metadata?.projectDisplayName;
  return typeof value === 'string' && value.trim() ? value.trim() : workspace.name;
}

type WorkspaceAvailabilityIssue = 'missing-files' | 'permission-needed' | undefined;

async function workspaceAvailabilityIssue(
  core: ReturnType<typeof useCoreServices>,
  workspace: WorkspaceInfo,
): Promise<WorkspaceAvailabilityIssue> {
  if (workspace.type === WORKSPACE_STORAGE_TYPE.NativeFS) {
    const handle = workspace.metadata?.rootDirHandle;
    if (!handle || typeof handle !== 'object' || (handle as { kind?: unknown }).kind !== 'directory') return 'missing-files';
    try {
      return await hasPermission(handle as FileSystemDirectoryHandle, 'read') ? undefined : 'permission-needed';
    } catch {
      return 'permission-needed';
    }
  }
  if (workspace.type !== WORKSPACE_STORAGE_TYPE.Browser && workspace.type !== WORKSPACE_STORAGE_TYPE.BrowserOPFS) return undefined;
  try {
    const provider = await core.projectStorage.getProvider(workspace.name);
    const entries = await provider.listEntries();
    // Every normal project creation/import path materializes at least one file. Treat only
    // a completely empty browser provider as unavailable so renamed/custom projects are
    // not falsely marked missing merely because their historical entry file changed.
    return entries.some((entry) => entry.kind === 'file') ? undefined : 'missing-files';
  } catch {
    return 'missing-files';
  }
}

function relativeTime(value?: string | null): string | undefined {
  if (!value) return undefined;
  const timestamp = new Date(value).getTime();
  if (!Number.isFinite(timestamp)) return undefined;
  const seconds = Math.round((timestamp - Date.now()) / 1000);
  const abs = Math.abs(seconds);
  const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
  if (abs < 60) return formatter.format(seconds, 'second');
  const minutes = Math.round(seconds / 60);
  if (Math.abs(minutes) < 60) return formatter.format(minutes, 'minute');
  const hours = Math.round(minutes / 60);
  if (Math.abs(hours) < 24) return formatter.format(hours, 'hour');
  const days = Math.round(hours / 24);
  if (Math.abs(days) < 30) return formatter.format(days, 'day');
  const months = Math.round(days / 30);
  if (Math.abs(months) < 12) return formatter.format(months, 'month');
  return formatter.format(Math.round(months / 12), 'year');
}

function workspaceProjectProfile(workspace: WorkspaceInfo): ProjectProfile | undefined {
  const metadata = workspace.metadata as Record<string, unknown> | undefined;
  if (isProjectProfile(metadata?.projectProfile)) return metadata.projectProfile;
  const mode = projectModeById(typeof metadata?.projectModeId === 'string' ? metadata.projectModeId : undefined);
  return mode ? canonicalProfileForMode(mode.id) : undefined;
}

function workspaceRootFile(workspace: WorkspaceInfo): string | undefined {
  const profile = workspaceProjectProfile(workspace);
  const mode = projectModeForProfile(profile);
  const entry = profile?.entry || mode?.defaultEntryFile;
  if (entry) return entry;
  const metadata = workspace.metadata as Record<string, unknown> | undefined;
  const project = metadata?.project;
  if (!project || typeof project !== 'object' || Array.isArray(project)) return undefined;
  const rootFile = (project as Record<string, unknown>).rootFile;
  return typeof rootFile === 'string' && rootFile.trim() ? rootFile.trim() : undefined;
}

function workspaceOpenHref(core: ReturnType<typeof useCoreServices>, workspace: WorkspaceInfo): string {
  const rootFile = workspaceRootFile(workspace);
  if (rootFile) {
    const parsed = WsPath.safeFromParts(workspace.name, rootFile);
    if (parsed.ok && parsed.data) return core.navigation.toWsFileUri(parsed.data.wsPath);
  }
  return core.navigation.toUri({ route: 'ws-home', payload: { wsName: workspace.name } });
}

async function detectProjectEntry(core: ReturnType<typeof useCoreServices>, wsName: string): Promise<string | undefined> {
  try {
    const targets = await core.projectStorage.detectProjectTargets(wsName);
    const profile = targets[0]?.profile;
    if (!profile) return undefined;
    const persisted = await core.projectStorage.updateProjectProfile(wsName, profile);
    return persisted.entry || projectModeForProfile(persisted)?.defaultEntryFile;
  } catch {
    return undefined;
  }
}

async function deleteBrowserProjectContents(core: ReturnType<typeof useCoreServices>, wsName: string): Promise<void> {
  const provider = await core.projectStorage.getProvider(wsName);
  const entries = await provider.listEntries();
  for (const entry of entries) {
    if (entry.kind === 'file') await provider.deleteFile(entry.path);
  }
  const directories = entries
    .filter((entry): entry is Extract<ProjectEntry, { kind: 'directory' }> => entry.kind === 'directory')
    .sort((a, b) => b.path.length - a.path.length);
  for (const entry of directories) await provider.deleteDirectory(entry.path).catch(() => undefined);
}

function projectDateValue(value: string | number | null | undefined): number {
  if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
  if (typeof value !== 'string' || !value.trim()) return 0;
  const parsed = Date.parse(value);
  return Number.isFinite(parsed) ? parsed : 0;
}

function formatBytes(value: number): string { if (!Number.isFinite(value) || value <= 0) return '0 B'; const units=['B','KB','MB','GB','TB']; let amount=value; let index=0; while(amount>=1000&&index<units.length-1){amount/=1000;index+=1;} return `${amount>=10||index===0?amount.toFixed(0):amount.toFixed(1)} ${units[index]}`; }
function formatDurationSeconds(value: number): string { if (!Number.isFinite(value) || value < 0) return '—'; const seconds=Math.floor(value); const days=Math.floor(seconds/86400); const hours=Math.floor((seconds%86400)/3600); const minutes=Math.floor((seconds%3600)/60); if(days>0)return `${days}d ${hours}h`; if(hours>0)return `${hours}h ${minutes}m`; return `${Math.max(0,minutes)}m`; }

async function materializeForkSnapshot(core:ReturnType<typeof useCoreServices>,project:CloudProject,publication:ProjectPublication):Promise<WorkspaceInfo>{
  const snapshot=publication.snapshot||{};const name=await uniqueWorkspaceName(project.projectSlug||project.name,core.workspaceOps);
  const type=supportsOpfsProjectStorage()?WORKSPACE_STORAGE_TYPE.BrowserOPFS:WORKSPACE_STORAGE_TYPE.Browser;
  const info=await core.workspaceOps.createWorkspaceInfo({name,type,metadata:{product:'learnitive',projectDisplayName:project.name,forkedFromPublication:publication.version}});if(!info)throw new Error('Unable to create fork workspace.');
  try{const provider=await core.projectStorage.getProvider(name);const encoder=new TextEncoder();for(const [path,content] of Object.entries(snapshot))await provider.writeFile(projectFilePath(path),encoder.encode(content));const targets=await core.projectStorage.detectProjectTargets(name);if(targets[0]?.profile)await core.projectStorage.updateProjectProfile(name,targets[0].profile);await requestPersistentBrowserStorage();await finalizeCreatedBrowserProject(name,core);await core.workspaceState.refreshWorkspacesNow();return (await core.workspaceOps.getWorkspaceInfo(name))||info;}catch(error){await core.workspaceOps.deleteWorkspaceInfo(name).catch(()=>undefined);throw error;}
}

function ProjectsPage() {
  const core = useCoreServices();
  const { config } = useSaasSession();
  const revision = useAtomValue(core.workspaceOps.$workspaceInfoChange);
  const [locals, setLocals] = React.useState<WorkspaceInfo[]>([]);
  const [browserProjectLimit, setBrowserProjectLimit] = React.useState<{ current: number; limit: number | null; reached: boolean }>({ current: 0, limit: 5, reached: false });
  const [loading, setLoading] = React.useState(true);
  const [gitOpen, setGitOpen] = React.useState(false);
  const [projectQuery, setProjectQuery] = React.useState('');
  const [selectedProjects, setSelectedProjects] = React.useState<Set<string>>(() => new Set());
  const [unavailableLocalNames, setUnavailableLocalNames] = React.useState<Set<string>>(() => new Set());
  const [storageOpen, setStorageOpen] = React.useState(false);
  const [folderSnapshot, setFolderSnapshot] = React.useState<Awaited<ReturnType<ProjectMirrorService['getSnapshot']>>>();
  const importRef = React.useRef<HTMLInputElement>(null);

  const mirrorService = React.useMemo(() => new ProjectMirrorService({
    workspaceOps: core.workspaceOps,
    projectStorage: core.projectStorage,
    workspaceState: core.workspaceState,
  }), [core.projectStorage, core.workspaceOps, core.workspaceState]);

  const refreshFolderSnapshot = React.useCallback(async () => {
    setFolderSnapshot(await mirrorService.getSnapshot().catch(() => undefined));
  }, [mirrorService]);

  const refresh = React.useCallback(async () => {
    setLoading(true);
    let localRows = await core.workspaceOps.getAllWorkspaces();
    let normalizedLegacyProject = false;
    for (const workspace of localRows) {
      if (workspaceProjectProfile(workspace)) continue;
      if (await detectProjectEntry(core, workspace.name)) normalizedLegacyProject = true;
    }
    if (normalizedLegacyProject) localRows = await core.workspaceOps.getAllWorkspaces();
    localRows = localRows.filter((workspace) => !isTemporaryLearnWorkspace(workspace));
    const unavailable = new Set<string>();
    await Promise.all(localRows.map(async (workspace) => {
      const issue = await workspaceAvailabilityIssue(core, workspace);
      if (issue) unavailable.add(workspace.name);
    }));
    setUnavailableLocalNames(unavailable);
    setLocals(localRows);
    try { setBrowserProjectLimit(await core.workspaceOps.getBrowserProjectLimitStatus()); }
    catch {
      const fallbackLimit = config?.freePlanProjectLimit ?? 5;
      const fallbackCurrent = localRows.filter((workspace) => workspace.type === WORKSPACE_STORAGE_TYPE.Browser || workspace.type === WORKSPACE_STORAGE_TYPE.BrowserOPFS || workspace.type === WORKSPACE_STORAGE_TYPE.NativeFS).length;
      setBrowserProjectLimit({ current: fallbackCurrent, limit: fallbackLimit, reached: fallbackCurrent >= fallbackLimit });
    }
    await refreshFolderSnapshot();
    setLoading(false);
  }, [config?.freePlanProjectLimit, core, refreshFolderSnapshot]);

  React.useEffect(() => { void revision; void refresh(); }, [refresh, revision]);
  React.useEffect(() => {
    const listener = () => { void refreshFolderSnapshot(); void refresh(); };
    window.addEventListener(PROJECTS_FOLDER_STATE_EVENT, listener);
    return () => window.removeEventListener(PROJECTS_FOLDER_STATE_EVENT, listener);
  }, [refresh, refreshFolderSnapshot]);

  const projectRows = locals
    .map((workspace) => ({ workspace, unavailable: unavailableLocalNames.has(workspace.name), recency: workspace.lastModified || 0 }))
    .sort((a, b) => b.recency - a.recency || workspaceProjectDisplayName(a.workspace).localeCompare(workspaceProjectDisplayName(b.workspace)));
  const projectRowKey = (row: (typeof projectRows)[number]) => `local:${row.workspace.name}`;
  const projectModeLabel = (workspace: WorkspaceInfo) => projectModeForProfile(workspaceProjectProfile(workspace))?.label || 'Project';
  const filteredProjectRows = projectRows.filter((row) => {
    const needle = projectQuery.trim().toLocaleLowerCase();
    if (!needle) return true;
    return `${workspaceProjectDisplayName(row.workspace)} ${row.workspace.name} ${projectModeLabel(row.workspace)} ${row.unavailable ? 'missing unavailable' : ''}`.toLocaleLowerCase().includes(needle);
  });
  const allVisibleSelected = filteredProjectRows.length > 0 && filteredProjectRows.every((row) => selectedProjects.has(projectRowKey(row)));
  const someVisibleSelected = filteredProjectRows.some((row) => selectedProjects.has(projectRowKey(row)));
  const toggleProjectSelection = (key: string) => setSelectedProjects((current) => { const next = new Set(current); if (next.has(key)) next.delete(key); else next.add(key); return next; });
  const toggleAllVisibleProjects = () => setSelectedProjects((current) => { const next = new Set(current); if (allVisibleSelected) for (const row of filteredProjectRows) next.delete(projectRowKey(row)); else for (const row of filteredProjectRows) next.add(projectRowKey(row)); return next; });
  React.useEffect(() => { setSelectedProjects(new Set()); }, [projectQuery]);

  const projectLimitReached = browserProjectLimit.limit !== null && browserProjectLimit.reached;
  const projectLimitMessage = browserProjectLimit.limit === null ? '' : `Free includes up to ${browserProjectLimit.limit} projects in this browser. Delete a project or upgrade to Pro to create another.`;
  const projectUsageLabel = browserProjectLimit.limit === null ? `${browserProjectLimit.current} project${browserProjectLimit.current === 1 ? '' : 's'} · Pro unlimited` : `${browserProjectLimit.current} of ${browserProjectLimit.limit} projects · browser-local limit`;
  const openCreate = () => { if (projectLimitReached) { toast.error(projectLimitMessage); return; } core.commandDispatcher.dispatch('command::ui:create-workspace-dialog', null, 'ui'); };

  const importZip = async (file: File) => {
    const displayName = file.name.replace(/\.zip$/iu, '').trim() || 'Imported Project';
    try {
      if (projectLimitReached) throw new Error(projectLimitMessage);
      const name = await uniqueWorkspaceName(displayName, core.workspaceOps);
      const type = supportsOpfsProjectStorage() ? WORKSPACE_STORAGE_TYPE.BrowserOPFS : WORKSPACE_STORAGE_TYPE.Browser;
      await core.workspaceOps.createWorkspaceInfo({ name, type, metadata: { product: 'learnitive', projectDisplayName: displayName } });
      await core.projectStorage.importZipBlob(name, file);
      await detectProjectEntry(core, name);
      await finalizeCreatedBrowserProject(name, core);
      await core.workspaceState.refreshWorkspacesNow();
      toast.success(`Imported ${name}.`);
      await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
  };

  const onGitCloned = async (name: string) => {
    const rootFile = await detectProjectEntry(core, name);
    await core.workspaceState.refreshWorkspacesNow();
    await refresh();
    const href = rootFile ? core.navigation.toWsFileUri(WsPath.fromParts(name, rootFile).wsPath) : core.navigation.toUri({ route: 'ws-home', payload: { wsName: name } });
    window.location.href = href;
  };

  const rename = async (workspace: WorkspaceInfo) => {
    const currentName = workspaceProjectDisplayName(workspace);
    const requested = (await requestPagePrompt({ title: 'Rename project', description: `This changes the display name only. The stable project slug “${workspaceProjectSlug(workspace)}” and synchronized folder name will not change.`, label: 'Project name', initialValue: currentName, confirmText: 'Rename' }))?.trim();
    if (!requested || requested === currentName) return;
    try {
      await core.workspaceOps.updateWorkspaceMetadata(workspace.name, (metadata) => ({ ...metadata, projectDisplayName: requested }));
      await mirrorService.syncProject(workspace.name).catch(() => undefined);
      toast.success(`Renamed to ${requested}.`);
      await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
  };

  const downloadProject = async (workspace: WorkspaceInfo) => { try { await core.projectCollaboration.flushToStorage(workspace.name); const archive = await core.projectStorage.exportZipStream(workspace.name); await saveZipStream(archive, safeDownloadName(workspaceProjectSlug(workspace))); toast.success(`Downloaded ${workspace.name}.`); } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); } };

  const duplicateProject = async (workspace: WorkspaceInfo) => {
    try {
      if (projectLimitReached) throw new Error(projectLimitMessage);
      await core.projectCollaboration.flushToStorage(workspace.name).catch(() => undefined);
      const sourceDisplayName = workspaceProjectDisplayName(workspace);
      const displayName = `${sourceDisplayName} Copy`;
      const name = await uniqueWorkspaceName(`${workspaceProjectSlug(workspace)}-copy`, core.workspaceOps);
      const type = supportsOpfsProjectStorage() ? WORKSPACE_STORAGE_TYPE.BrowserOPFS : WORKSPACE_STORAGE_TYPE.Browser;
      const metadata = { ...(workspace.metadata ?? {}) } as Record<string, unknown>;
      for (const key of ['rootDirHandle','projectsRootHandle','projectsRootChildName','saasProjectId','saasPendingPatch','projectId','projectSlug','projectCreatedAt','projectDisplayName']) delete metadata[key];
      metadata.product = 'learnitive';
      metadata.projectDisplayName = displayName;
      const createdLocal = await core.workspaceOps.createWorkspaceInfo({ name, type, metadata });
      if (!createdLocal) throw new Error('Unable to create the project copy.');
      try {
        const source = await core.projectStorage.getProvider(workspace.name); const target = await core.projectStorage.getProvider(name); await copyProviderTree(source, target); await detectProjectEntry(core, name);
      } catch (reason) { await deleteBrowserProjectContents(core, name).catch(() => undefined); await core.workspaceOps.deleteWorkspaceInfo(name).catch(() => undefined); throw reason; }
      await finalizeCreatedBrowserProject(name, core);
      await core.workspaceState.refreshWorkspacesNow(); core.fileSystem.refreshFileTree(); toast.success(`Duplicated ${sourceDisplayName} as ${displayName}.`); await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
  };

  const performLocalProjectDeletion = async (workspace: WorkspaceInfo): Promise<void> => {
    const linked = workspace.type === WORKSPACE_STORAGE_TYPE.NativeFS;
    await core.projectGit.discardLocalRepository(workspace.name).catch(() => undefined);
    if (!linked) {
      await mirrorService.trashProject(workspace).catch(() => undefined);
      await core.projectCollaboration.flushToStorage(workspace.name).catch(() => undefined);
      if (unavailableLocalNames.has(workspace.name)) await deleteBrowserProjectContents(core, workspace.name).catch(() => undefined); else await deleteBrowserProjectContents(core, workspace.name);
    }
    await core.workspaceOps.deleteWorkspaceInfo(workspace.name);
  };

  const deleteProject = async (workspace: WorkspaceInfo) => {
    const linked = workspace.type === WORKSPACE_STORAGE_TYPE.NativeFS;
    const mirrorConnected = Boolean(folderSnapshot?.connected);
    const message = linked
      ? `Remove legacy external project ${workspace.name} from Learnitive? Its external folder and files will remain on your device.`
      : mirrorConnected
        ? `Delete ${workspace.name}? Its Browser copy will be deleted and its synchronized copy will be moved to Learnitive local trash.`
        : `Delete ${workspace.name}? Its Browser copy will be deleted. If an older disconnected Projects Folder still contains this project, that backup may be offered again during a future recovery.`;
    if (!(await requestPageConfirm({ title: linked ? 'Remove external project?' : 'Delete project?', description: message, confirmText: linked ? 'Remove from Learnitive' : 'Delete project', tone: 'destructive' }))) return;
    try { await performLocalProjectDeletion(workspace); await core.workspaceState.refreshWorkspacesNow(); core.fileSystem.refreshFileTree(); toast.success(linked ? 'External project removed from Learnitive. Its folder was not deleted.' : 'Project deleted.'); await refresh(); }
    catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
  };

  const deleteSelectedProjects = async () => {
    const rows = projectRows.filter((row) => selectedProjects.has(projectRowKey(row)));
    if (!rows.length) return;
    const browserCount = rows.filter((row) => row.workspace.type !== WORKSPACE_STORAGE_TYPE.NativeFS).length;
    const externalCount = rows.length - browserCount;
    const details = [browserCount ? `${browserCount} Browser project${browserCount === 1 ? '' : 's'} will be deleted${folderSnapshot?.connected ? ' and synchronized copies moved to local trash' : ''}.` : '', externalCount ? `${externalCount} legacy external project link${externalCount === 1 ? '' : 's'} will be removed without deleting external files.` : ''].filter(Boolean).join(' ');
    if (!(await requestPageConfirm({ title: `Delete ${rows.length} selected project${rows.length === 1 ? '' : 's'}?`, description: details, confirmText: 'Delete selected', tone: 'destructive' }))) return;
    const failures: string[] = []; let removed = 0;
    for (const row of rows) {
      try { await performLocalProjectDeletion(row.workspace); removed += 1; }
      catch (reason) { failures.push(`${row.workspace.name}: ${reason instanceof Error ? reason.message : String(reason)}`); }
    }
    setSelectedProjects(new Set()); await core.workspaceState.refreshWorkspacesNow(); core.fileSystem.refreshFileTree(); await refresh();
    if (failures.length) toast.error(`${removed} removed, ${failures.length} failed. ${failures.slice(0, 2).join(' · ')}`);
    else toast.success(`${removed} project${removed === 1 ? '' : 's'} removed.`);
  };

  const emptyProjects = !loading && projectRows.length === 0;
  const storageButtonLabel = folderSnapshot?.status === 'permission-required' ? 'Storage · Access required' : folderSnapshot?.status === 'conflict' ? 'Storage · Conflict' : folderSnapshot?.connected ? 'Storage · Synced' : 'Storage';

  return <section className={`interactive-projects-page${emptyProjects ? ' interactive-projects-page--empty' : ''}`}>
    <input ref={importRef} className="sr-only" type="file" accept=".zip,application/zip" onChange={(event) => { const file = event.currentTarget.files?.[0]; event.currentTarget.value = ''; if (file) void importZip(file); }} />
    <CloneGitRepositoryDialog open={gitOpen} onOpenChange={setGitOpen} navigateOnSuccess={false} onCloned={onGitCloned} />
    <ProjectStorageDialog open={storageOpen} onOpenChange={setStorageOpen} />

    <div className="interactive-app-title-row">
      <div><span className="interactive-eyebrow">Local first workspace</span><h1 className="interactive-display">Projects</h1><p>Projects live in this browser. Connect one optional Projects Folder to synchronize all projects with ordinary files and recover them after browser data is cleared — no account required.</p>{projectUsageLabel ? <p className={projectLimitReached ? 'text-destructive text-sm' : 'interactive-muted text-sm'}>{projectUsageLabel}</p> : null}</div>
      <div className="interactive-project-primary-actions">
        <Button variant="outline" disabled={projectLimitReached} onClick={() => importRef.current?.click()} data-testid="projects-import-zip"><Upload className="size-4" /> Import ZIP</Button>
        <Button disabled={projectLimitReached} onClick={openCreate}><Plus className="size-4" /> New Project</Button>
        <Button type="button" variant={folderSnapshot?.status === 'synced' ? 'default' : 'outline'} onClick={() => setStorageOpen(true)}><FolderOpen className="size-4" /> {storageButtonLabel}</Button>
        <DismissibleDetails className="interactive-import-menu interactive-project-more-menu"><summary className="interactive-icon-button" aria-label="More project actions"><MoreHorizontal className="size-4" /></summary><div className="interactive-import-popover"><a href="/templates"><LayoutTemplate className="size-4" /> Templates</a><button type="button" disabled={projectLimitReached} onClick={() => setGitOpen(true)}><GitBranch className="size-4" /> Import from GitHub</button></div></DismissibleDetails>
      </div>
    </div>

    {folderSnapshot?.status === 'permission-required' ? <div className="interactive-storage-recovery-banner"><div><strong>Projects Folder access required</strong><p>Browser projects continue working. Grant folder access to resume synchronization.</p></div><Button variant="outline" onClick={() => setStorageOpen(true)}>Grant access</Button></div> : null}

    {!loading && projectRows.length ? <div className="interactive-project-management-toolbar">
      <label className="interactive-project-select-all"><input type="checkbox" checked={allVisibleSelected} ref={(node)=>{if(node)node.indeterminate=!allVisibleSelected&&someVisibleSelected;}} aria-checked={!allVisibleSelected && someVisibleSelected ? 'mixed' : allVisibleSelected} onChange={toggleAllVisibleProjects}/><span>Select all</span></label>
      <label className="interactive-project-search"><Search className="size-4" aria-hidden/><input aria-label="Search projects" placeholder="Search projects" value={projectQuery} onChange={(event)=>setProjectQuery(event.currentTarget.value)}/></label>
      {selectedProjects.size ? <Button type="button" variant="destructive" onClick={()=>void deleteSelectedProjects()}><Trash2 className="size-4"/> Delete selected ({selectedProjects.size})</Button> : null}
    </div> : null}

    {loading ? <LoadingCard /> : <div className={`interactive-project-list${emptyProjects ? ' interactive-project-list--empty' : ''}`}>
      {filteredProjectRows.map((row) => {
        const { workspace } = row;
        const href = workspaceOpenHref(core, workspace);
        const legacyExternal = workspace.type === WORKSPACE_STORAGE_TYPE.NativeFS;
        const note = row.unavailable
          ? [projectModeLabel(workspace), legacyExternal ? 'Legacy external project unavailable' : 'Files are not available in this browser'].join(' · ')
          : [projectModeLabel(workspace), legacyExternal ? 'Legacy external project' : folderSnapshot?.connected ? 'Projects Folder sync enabled' : 'Browser'].join(' · ');
        const key=projectRowKey(row);
        return <article key={key} className="interactive-project-row" data-status={row.unavailable ? 'missing' : 'local'}>
          <label className="interactive-project-row-select"><input type="checkbox" checked={selectedProjects.has(key)} onChange={()=>toggleProjectSelection(key)} aria-label={`Select ${workspaceProjectDisplayName(workspace)}`}/></label>
          <div className="interactive-project-icon"><FileCode2 className="size-5" /></div>
          <div className="interactive-project-copy"><div className="interactive-project-title-line">{row.unavailable ? <h2>{workspaceProjectDisplayName(workspace)}</h2> : <a href={href}><h2>{workspaceProjectDisplayName(workspace)}</h2></a>}</div><p>{note}</p></div>
          <div className="interactive-project-row-actions">{!row.unavailable ? <a className="interactive-button interactive-button-secondary interactive-button-sm" href={href}>Open</a> : null}<DismissibleDetails><summary className="interactive-icon-button" aria-label={`Project actions for ${workspaceProjectDisplayName(workspace)}`}><MoreHorizontal className="size-4" /></summary><div className="interactive-row-menu">{!row.unavailable ? <><button type="button" onClick={() => void downloadProject(workspace)}><Download className="size-4" /> Download ZIP</button><button type="button" disabled={projectLimitReached} onClick={() => void duplicateProject(workspace)}><Copy className="size-4" /> Duplicate</button></> : null}<button type="button" className="interactive-row-menu-action" onClick={() => void rename(workspace)}><Pencil className="size-4" /><span>Rename</span><ChevronRight className="interactive-menu-trailing size-3.5" /></button><button type="button" className="danger" onClick={() => void deleteProject(workspace)}><Trash2 className="size-4" /> {legacyExternal ? 'Remove External Project' : 'Delete Project'}</button></div></DismissibleDetails></div>
        </article>;
      })}
      {!emptyProjects && filteredProjectRows.length===0 ? <div className="interactive-empty-state"><Search className="size-6"/><h2>No matching projects.</h2><p>Try another search.</p></div> : null}
      {emptyProjects ? <div className="interactive-empty-state interactive-projects-empty-state"><h2 className="interactive-display">Your projects start here.</h2><p className="interactive-projects-empty-copy">Create a Browser project or recover a previously synchronized Projects Folder. Recovery works even after browser data is fully cleared and does not require an account.</p><div className="flex flex-wrap justify-center gap-2"><Button variant="outline" onClick={() => setStorageOpen(true)}><FolderOpen className="size-4" /> Recover Projects Folder</Button><Button variant="outline" disabled={projectLimitReached} onClick={() => importRef.current?.click()}><Upload className="size-4" /> Import ZIP</Button><Button disabled={projectLimitReached} onClick={openCreate}><Plus className="size-4" /> New Project</Button></div></div> : null}
    </div>}
  </section>;
}

async function copyProviderTree(source:ProjectStorageProvider,target:ProjectStorageProvider){const entries=await source.listEntries();for(const entry of entries){if(entry.kind!=='file')continue;const bytes=await source.readFile(entry.path);if(bytes)await target.writeFile(entry.path,bytes)}}
function ExploreProjectsPage(){
  const core=useCoreServices();const {auth,loading}=useSaasSession();const [projects,setProjects]=React.useState<DiscoveredProject[]>([]);const [busy,setBusy]=React.useState<string>();const [error,setError]=React.useState('');
  React.useEffect(()=>{if(!auth.authenticated)return;let cancelled=false;void discoverPublishedProjects().then(rows=>{if(!cancelled)setProjects(rows)}).catch(reason=>{if(!cancelled)setError(reason instanceof Error?reason.message:String(reason))});return()=>{cancelled=true}},[auth.authenticated]);
  if(loading)return <LoadingCard/>;if(!auth.authenticated)return <AuthRequired title="Sign in to explore published projects"/>;
  const fork=async(project:DiscoveredProject)=>{setBusy(project.id);setError('');try{const result=await forkPublishedProject(project.id);const workspace=await materializeForkSnapshot(core,result.project,result.publication);toast.success(`Forked ${project.name} into your local workspace.`);window.location.assign(workspaceOpenHref(core,workspace));}catch(reason){setError(reason instanceof Error?reason.message:String(reason));}finally{setBusy(undefined)}};
  return <section className="interactive-page-container space-y-6 py-8"><header><span className="interactive-eyebrow">Authenticated community</span><h1 className="interactive-display">Explore projects</h1><p>Browse snapshots explicitly published by signed-in Learnitive users. Forking creates your own local-first project; it never edits the publisher's sandbox.</p></header>{error?<ErrorNotice>{error}</ErrorNotice>:null}{projects.length?<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{projects.map(project=><article key={project.id} className="rounded-xl border p-4 space-y-3"><div><h2 className="font-semibold text-lg">{project.name}</h2><p className="text-sm text-muted-foreground">Snapshot v{project.publication.version}{project.publication.createdAt?` · ${relativeTime(project.publication.createdAt)}`:''}</p></div><div className="text-xs text-muted-foreground">{project.publication.manifest?.fileCount??'—'} source files · {formatBytes(project.publication.manifest?.totalBytes??0)}</div><Button disabled={busy===project.id} onClick={()=>void fork(project)}>{busy===project.id?<LoaderCircle className="size-4 animate-spin"/>:<GitBranch className="size-4"/>} Fork to my projects</Button></article>)}</div>:<div className="interactive-empty-state"><Users className="size-7"/><h2>No published projects yet</h2><p>Projects appear here only after their owners explicitly publish a snapshot.</p><a className="interactive-button interactive-button-secondary" href="/projects">Projects</a></div>}</section>;
}

function SettingsPage() {
  const { auth, refresh } = useSaasSession();
  const core = useCoreServices();
  const preference = useAtomValue(core.workbenchState.$themePref);
  const [name, setName] = React.useState(auth.authenticated ? auth.user.displayName : '');
  const [usage,setUsage]=React.useState<AccountUsageDay[]>([]);
  const [storageOpen, setStorageOpen] = React.useState(false);
  React.useEffect(() => { if (auth.authenticated) { setName(auth.user.displayName); void getAccountUsage(30).then(setUsage).catch(()=>setUsage([])); } }, [auth]);
  if (!auth.authenticated) return <AuthRequired title="Sign in to manage account settings." />;

  const save = async () => {
    try { await updateProfile(name); toast.success('Profile updated.'); await refresh(); }
    catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
  };
  const setTheme = async (value: 'light' | 'dark' | 'system') => {
    core.workbenchState.changeThemePreference(value);
    try { await updateSafeSettings({ theme: value }); } catch { /* local preference remains authoritative offline */ }
  };
  const remove = async () => {
    if (!(await requestPageConfirm({ title: 'Delete Learnitive account?', description: 'This deletes or anonymizes SaaS account data. Local project files on this device will not be deleted.', confirmText: 'Delete account', tone: 'destructive' }))) return;
    try { await deleteAccount(); window.location.href = '/'; }
    catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
  };
  const licenseVerifiedAt = auth.entitlement.license?.lastVerifiedAt ?? auth.entitlement.license?.verifiedAt;
  const licenseType = auth.entitlement.license?.type === 'monthly' ? 'Pro Monthly' : auth.entitlement.license?.type === 'lifetime' ? 'Pro Lifetime' : auth.entitlement.pro ? 'Pro' : 'Free';
  const licenseExpiresAt = auth.entitlement.license?.expiresAt;
  const entitlementStatus = auth.entitlement.blocked ? 'Blocked' : auth.entitlement.expired ? 'Free · Monthly expired' : auth.entitlement.pro ? licenseType : 'Free';
  const projectAllowance = auth.entitlement.projectLimit == null ? 'Unlimited' : `Up to ${auth.entitlement.projectLimit}`;
  const usageTotals=usage.reduce((total,row)=>({sandboxCreated:total.sandboxCreated+row.sandboxCreated,sandboxRecreated:total.sandboxRecreated+row.sandboxRecreated,aiTasks:total.aiTasks+row.aiTasks,commands:total.commands+row.commands,processStarts:total.processStarts+row.processStarts,storagePeakBytes:Math.max(total.storagePeakBytes,row.storagePeakBytes)}),{sandboxCreated:0,sandboxRecreated:0,aiTasks:0,commands:0,processStarts:0,storagePeakBytes:0});
  return <section>
    <ProjectStorageDialog open={storageOpen} onOpenChange={setStorageOpen} />
    <div className="interactive-app-title-row"><div><span className="interactive-eyebrow">Account</span><h1 className="interactive-display">Settings</h1><p>Safe account metadata only. Local project files remain outside the account service.</p></div></div>
    <div className="interactive-settings-grid">
      <section className="interactive-settings-card interactive-settings-profile-card">
        <h2>Profile</h2>
        <div className="interactive-profile-row">{auth.user.avatarUrl ? <img src={auth.user.avatarUrl} alt="" referrerPolicy="no-referrer" /> : <CircleUserRound className="size-10" />}<div><strong>{auth.user.email}</strong></div></div>
        <Label htmlFor="display-name">Display name</Label>
        <Input id="display-name" value={name} onChange={(e) => setName(e.currentTarget.value)} />
        <div className="interactive-account-metadata" role="group" aria-label="Account details">
          <span>Provider: {auth.user.provider || 'Account'}</span>
          <span>Joined: {auth.user.createdAt ? new Date(auth.user.createdAt).toLocaleDateString() : '—'}</span>
          <span>Last sign-in: {auth.user.lastLoginAt ? new Date(auth.user.lastLoginAt).toLocaleDateString() : '—'}</span>
        </div>
        <Button onClick={() => void save()}>Save profile</Button>
      </section>
      <div className="interactive-settings-entitlement-stack">
        <section className="interactive-settings-card"><h2>Plan</h2><div className="interactive-stat-line interactive-stat-line-compact"><span>Status</span><strong>{entitlementStatus}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Projects</span><strong>{projectAllowance}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>PDF export</span><strong>{auth.entitlement.pro ? 'Clean' : 'Learnitive watermark'}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Support</span><strong>{auth.entitlement.pro ? 'Priority' : 'Community'}</strong></div>{licenseVerifiedAt ? <div className="interactive-stat-line interactive-stat-line-compact"><span>License verified</span><strong>{new Date(licenseVerifiedAt).toLocaleDateString()}</strong></div> : null}{licenseExpiresAt ? <div className="interactive-stat-line interactive-stat-line-compact"><span>Monthly access until</span><strong>{new Date(licenseExpiresAt).toLocaleDateString()}</strong></div> : null}</section>
        <LicenseLinkCard inputId="settings-license-key" />
      </div>
      <section className="interactive-settings-card"><h2>Appearance</h2><div className="interactive-segmented interactive-settings-theme">{(['system', 'light', 'dark'] as const).map((value) => <button type="button" key={value} data-active={preference === value} onClick={() => void setTheme(value)}>{value}</button>)}</div><p className="interactive-muted">Theme is saved locally immediately and synchronised as safe account metadata when available.</p></section>
      <section className="interactive-settings-card"><h2>Project Storage</h2><div className="interactive-stat-line interactive-stat-line-compact"><span>Browser Storage</span><strong>Enabled for Browser</strong></div><p className="interactive-muted">Projects work from browser-local storage. Connect one Projects Folder to synchronize all projects with ordinary files on this device and recover them after browser data loss.</p><Button type="button" variant="outline" onClick={() => setStorageOpen(true)}><FolderOpen className="size-4" /> Manage Projects Folder</Button></section>
      <section className="interactive-settings-card"><h2>Development usage · 30 days</h2><div className="interactive-stat-line interactive-stat-line-compact"><span>Sandbox starts / rebuilds</span><strong>{usageTotals.sandboxCreated} / {usageTotals.sandboxRecreated}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Agent tasks</span><strong>{usageTotals.aiTasks}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Commands / processes</span><strong>{usageTotals.commands} / {usageTotals.processStarts}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Peak sandbox storage</span><strong>{formatBytes(usageTotals.storagePeakBytes)}</strong></div><p className="interactive-muted">Account-level counters support quotas and abuse controls without recording source code, command output, or secret values.</p></section>
      <section className="interactive-settings-card"><h2>Session</h2><Button variant="outline" onClick={() => void logout().then(() => { window.location.href = '/'; })}><LogOut className="size-4" /> Logout</Button></section>
      <section className="interactive-settings-card interactive-danger-card"><h2>Delete account</h2><p>Deletes or anonymizes SaaS account data and invalidates sessions. It does not delete project files from your device.</p><Button variant="destructive" onClick={() => void remove()}>Delete account</Button></section>
    </div>
  </section>;
}

function AuthRequired({title}:{title:string}){return <div className="interactive-empty-state"><CircleUserRound className="size-7"/><h1 className="interactive-display">{title}</h1><a className="interactive-button interactive-button-primary" href="/login">Sign in</a><a className="interactive-text-link" href="/projects">Continue locally</a></div>}

// Admin UI is intentionally compact; every mutation remains server-role protected.
function AdminLoginPage(){const [email,setEmail]=React.useState('admin@learnitive.com');const [password,setPassword]=React.useState('');const [nextPassword,setNextPassword]=React.useState('');const [force,setForce]=React.useState(false);const [error,setError]=React.useState('');const login=async(e:React.FormEvent)=>{e.preventDefault();setError('');try{const result=await adminLogin(email,password);if(result.forcePasswordChange)setForce(true);else window.location.href='/admin'}catch(reason){setError(reason instanceof Error?reason.message:String(reason))}};const change=async(e:React.FormEvent)=>{e.preventDefault();try{await adminChangePassword(nextPassword);window.location.href='/admin'}catch(reason){setError(reason instanceof Error?reason.message:String(reason))}};return <div className="interactive-admin-login"><form onSubmit={force?change:login} className="interactive-auth-card"><Brand/><span className="interactive-eyebrow">Administration</span><h1 className="interactive-display">{force?'Set a secure admin password':'Admin sign in'}</h1>{error?<ErrorNotice>{error}</ErrorNotice>:null}{force?<><Label htmlFor="new-admin-password">New password</Label><Input id="new-admin-password" type="password" minLength={12} required value={nextPassword} onChange={(e)=>setNextPassword(e.currentTarget.value)}/><Button type="submit">Change password</Button></>:<><Label htmlFor="admin-email">Email</Label><Input id="admin-email" type="email" required value={email} onChange={(e)=>setEmail(e.currentTarget.value)}/><Label htmlFor="admin-password">Password</Label><Input id="admin-password" type="password" required value={password} onChange={(e)=>setPassword(e.currentTarget.value)}/><Button type="submit">Sign in</Button></>}</form></div>}

function AdminArea({pathname}:{pathname:string}){const {auth,loading}=useSaasSession();if(loading)return <div className="interactive-admin-login"><LoadingCard/></div>;if(!auth.authenticated||auth.user.role!=='admin'){window.location.replace('/admin/login');return null}if(auth.user.forcePasswordChange){window.location.replace('/admin/login');return null}let page:React.ReactNode;if(pathname==='/admin')page=<AdminDashboard/>;else if(pathname==='/admin/users')page=<AdminUsers/>;else if(pathname==='/admin/licenses')page=<AdminLicenses/>;else if(pathname==='/admin/support')page=<AdminSupport/>;else if(pathname==='/admin/runtime')page=<AdminRuntime/>;else if(pathname==='/admin/settings/integrations')page=<AdminIntegrations/>;else if(pathname.startsWith('/admin/templates/preview/'))page=<AdminTemplatePreview slug={decodeURIComponent(pathname.slice('/admin/templates/preview/'.length))}/>;else if(pathname==='/admin/templates')page=<AdminTemplates/>;else if(pathname==='/admin/template-categories')page=<AdminTemplateCategories/>;else if(pathname==='/admin/institutions')page=<AdminInstitutions/>;else if(pathname==='/admin/change-log')page=<AdminChangeLog/>;else if(pathname==='/admin/legal')page=<AdminLegalPages/>;else if(pathname==='/admin/learn')page=<AdminLearnCourseList/>;else if(pathname==='/admin/learn/categories')page=<AdminLearnCategoryList/>;else if(pathname==='/admin/learn/import')page=<AdminLearnImportPage/>;else if(pathname.startsWith('/admin/learn/preview/')){const previewMatch=pathname.match(/^\/admin\/learn\/preview\/(\d+)(?:\/(\d+))?$/u);page=previewMatch?<AdminLearnPreviewPage courseId={Number(previewMatch[1])} itemId={previewMatch[2]?Number(previewMatch[2]):undefined}/>:<NotFoundPage/>;}else if(pathname.startsWith('/admin/learn/courses/'))page=<AdminLearnCourseBuilder pathname={pathname}/>;else if(pathname==='/admin/blog')page=<AdminCmsListPage kind="blog"/>;else if(pathname==='/admin/docs')page=<AdminCmsListPage kind="docs"/>;else if(pathname==='/admin/blog/new')page=<AdminCmsEditorPage kind="blog"/>;else if(pathname==='/admin/docs/new')page=<AdminCmsEditorPage kind="docs"/>;else {const cmsEditMatch=pathname.match(/^\/admin\/(blog|docs)\/(\d+)\/edit$/u);page=cmsEditMatch?<AdminCmsEditorPage kind={cmsEditMatch[1] as 'blog'|'docs'} id={Number(cmsEditMatch[2])}/>:<NotFoundPage/>;}return <div className="interactive-admin-shell"><aside><Brand/><nav><a href="/admin">Dashboard</a><a href="/admin/users">Users</a><a href="/admin/licenses">Licenses</a><a href="/admin/support">Support</a><a href="/admin/runtime">Runtime</a><a href="/admin/settings/integrations">Integrations</a><a href="/admin/templates">Templates</a><a href="/admin/template-categories">Categories</a><a href="/admin/institutions">Institutions</a><a href="/admin/change-log">Change log</a><a href="/admin/legal">Legal</a><a href="/admin/learn">LMS</a><a href="/admin/learn/categories">LMS categories</a><a href="/admin/learn/import">LMS import</a><a href="/admin/blog">Blog</a><a href="/admin/docs">Docs</a><a href="/projects">Projects</a></nav></aside><div><header className="interactive-admin-top"><ThemeControl/><AccountControl/></header><main>{page}</main></div></div>}
function useAdminData<T>(path:string){const [data,setData]=React.useState<T>();const [error,setError]=React.useState('');const refresh=React.useCallback(async()=>{try{setData(await adminGet<T>(path));setError('')}catch(reason){setError(reason instanceof Error?reason.message:String(reason))}},[path]);React.useEffect(()=>{void refresh()},[refresh]);return{data,error,refresh}}
function AdminPageHead({title,copy,actions}:{title:string;copy:string;actions?:React.ReactNode}){return <div className="interactive-admin-page-head"><div><h1 className="interactive-display">{title}</h1><p>{copy}</p></div>{actions?<div className="interactive-admin-page-actions">{actions}</div>:null}</div>}
function AdminDashboard(){const {data,error}=useAdminData<{metrics:Record<string,number>;authProviders?:Record<string,boolean>}>('/api/admin/dashboard');return <><AdminPageHead title="Dashboard" copy="Account layer operational metrics and authentication provider status. No project contents or secrets are included." actions={<a className="interactive-button interactive-button-secondary" href="/admin/change-log">Manage change log</a>}/>{error?<ErrorNotice>{error}</ErrorNotice>:null}<div className="interactive-admin-metrics">{data?Object.entries(data.metrics).map(([key,value])=><article key={key}><span>{key.replace(/([A-Z])/gu,' $1')}</span><strong>{value}</strong></article>):<LoadingCard/>}</div>{data?.authProviders?<section className="interactive-settings-card"><h2>Authentication</h2><p className="interactive-muted">Provider secrets are read from /etc/learnitive/learnitive.env and are never exposed here.</p><div className="interactive-admin-metrics">{Object.entries(data.authProviders).map(([key,enabled])=><article key={key}><span>{key.replace(/([A-Z])/gu,' $1')}</span><strong>{enabled?'Configured':'Disabled'}</strong></article>)}</div></section>:null}</>}

function AdminIntegrations(){
  const {data,error,refresh}=useAdminData<{configured:boolean;status:string}>('/api/admin/settings/integrations/youtube');const [apiKey,setApiKey]=React.useState('');const [busy,setBusy]=React.useState<'test'|'save'|''>('');const [message,setMessage]=React.useState('');
  const testConnection=async()=>{setBusy('test');setMessage('');try{await adminMutate('/api/admin/settings/integrations/youtube/test','POST',{});setMessage('YouTube Data API connection is working.');await refresh()}catch(reason){setMessage(reason instanceof Error?reason.message:String(reason))}finally{setBusy('')}};
  const save=async()=>{if(!apiKey.trim())return;setBusy('save');setMessage('');try{await adminMutate('/api/admin/settings/integrations/youtube/secret','PUT',{apiKey:apiKey.trim()});setApiKey('');setMessage('YouTube API key updated and verified.');await refresh()}catch(reason){setMessage(reason instanceof Error?reason.message:String(reason))}finally{setBusy('')}};
  return <><AdminPageHead title="Integrations" copy="Server-side provider connections. Secret values are never returned to the browser."/>{error?<ErrorNotice>{error}</ErrorNotice>:null}<section className="interactive-settings-card interactive-admin-integration-card"><div><span className="interactive-eyebrow">Video metadata</span><h2>YouTube Data API</h2><p className="interactive-muted">Used by Learn to preview and import YouTube playlists. The key is stored only in /etc/learnitive/learnitive.env.</p></div><div className="interactive-admin-integration-status"><span>Status</span><strong>{data?.configured?(data.status==='ok'?'Connected':'Configured'):'Not configured'}</strong></div><Label htmlFor="youtube-api-key">API key</Label><Input id="youtube-api-key" type="password" autoComplete="off" value={apiKey} placeholder={data?.configured?'••••••••••••••••••••':'Enter YouTube Data API key'} onChange={event=>setApiKey(event.currentTarget.value)}/><div className="interactive-admin-page-actions"><Button variant="outline" disabled={busy!==''||!data?.configured} onClick={()=>void testConnection()}>{busy==='test'?'Testing…':'Test connection'}</Button><Button disabled={busy!==''||!apiKey.trim()} onClick={()=>void save()}>{busy==='save'?'Updating…':'Update key'}</Button></div>{message?<p className="interactive-muted" role="status">{message}</p>:null}</section></>;
}

type AdminTerminalPlanPolicy = {
  maxLifetimeSeconds:number; idleTimeoutSeconds:number; workspaceRetentionSeconds:number;
  workspaceMaxBytes:number; uploadMaxBytes:number; downloadMaxBytes:number;
  memoryMb:number; cpuLimit:number; pidLimit:number; maxConcurrent:number; creationsPerHour:number;
  networkMode:'off'|'packages-git'|'internet'|string;
};
type AdminTerminalPlan = { planId:string; override:Partial<AdminTerminalPlanPolicy>; effective:AdminTerminalPlanPolicy; updatedAt?:string|null };
type AdminTerminalSandbox = { userId:string; containerName:string; type:'guest'|'authenticated'|string; generation:number; createdAt:string; expiresAt:string; status:string; activeSessions:number; connectedSockets:number; lastActivity?:string|null; networkMode?:string };
const ADMIN_TERMINAL_PLAN_LABELS:Record<string,string>={guest:'Guest',free:'Free',pro1:'Pro 1 Code',pro2:'Pro 2 Code',pro3:'Pro 3 Code'};
function adminDuration(seconds:number){const value=Number(seconds)||0;if(value%86400===0)return`${value/86400} day${value===86400?'':'s'}`;if(value%3600===0)return`${value/3600} hour${value===3600?'':'s'}`;if(value%60===0)return`${value/60} minutes`;return`${value} seconds`}
function adminBytes(bytes:number){const value=Number(bytes)||0;if(value>=1_000_000_000)return`${(value/1_000_000_000).toFixed(value%1_000_000_000?1:0)} GB`;if(value>=1_000_000)return`${(value/1_000_000).toFixed(value%1_000_000?1:0)} MB`;return`${value.toLocaleString()} bytes`}
type AdminHomeAiBuilderSettings = { enabled:boolean; anonymousVisible:boolean; freeUserEnabled:boolean; maxPromptLength:number; quickStarts:string[] };
const ADMIN_HOME_AI_QUICK_START_LABELS:Record<string,string>={'research-paper':'Research Paper',essay:'Essay','study-notes':'Study Notes','literature-review':'Literature Review',latex:'LaTeX',report:'Report',article:'Article',thesis:'Thesis',dissertation:'Dissertation','book-chapter':'Book Chapter',proposal:'Proposal','lab-report':'Lab Report','case-study':'Case Study','meeting-notes':'Meeting Notes',summary:'Summary','study-guide':'Study Guide',flashcards:'Flashcards',markdown:'Markdown'};
function AdminRuntime(){
  const {data,error,refresh}=useAdminData<{plans:AdminTerminalPlan[];allowedNetworkModes:string[];hardLimits:{pidLimit:number;maxConcurrent:number;creationsPerHour:number};requestLimits:{authenticatedPerMinute:number;guestPerMinute:number}}>('/api/admin/runtime/terminal-plans');
  const {data:sandboxData,error:sandboxError,refresh:refreshSandboxes}=useAdminData<{sandboxes:AdminTerminalSandbox[]}>('/api/admin/runtime/terminal-sandboxes');
  const {data:homeAiData,error:homeAiError,refresh:refreshHomeAi}=useAdminData<{settings:AdminHomeAiBuilderSettings;allowedQuickStarts:string[]}>('/api/admin/runtime/home-ai-builder');
  const [forms,setForms]=React.useState<Record<string,AdminTerminalPlanPolicy>>({});const [busy,setBusy]=React.useState('');const [sandboxBusy,setSandboxBusy]=React.useState('');
  const [homeAiForm,setHomeAiForm]=React.useState<AdminHomeAiBuilderSettings>();const [homeAiBusy,setHomeAiBusy]=React.useState(false);
  React.useEffect(()=>{if(!data)return;setForms(Object.fromEntries(data.plans.map((plan)=>[plan.planId,{...plan.effective}])));},[data]);
  React.useEffect(()=>{if(homeAiData)setHomeAiForm({...homeAiData.settings,quickStarts:[...homeAiData.settings.quickStarts]});},[homeAiData]);
  const update=(planId:string,key:keyof AdminTerminalPlanPolicy,value:number|string)=>setForms((current)=>({...current,[planId]:{...current[planId]!,[key]:value}}));
  const save=async(planId:string)=>{const form=forms[planId];if(!form)return;setBusy(planId);try{await adminMutate(`/api/admin/runtime/terminal-plans/${planId}`,'PATCH',form);toast.success(`${ADMIN_TERMINAL_PLAN_LABELS[planId]||planId} terminal policy saved.`);await refresh()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setBusy('')}};
  const toggleHomeAiQuickStart=(id:string)=>setHomeAiForm((current)=>{if(!current)return current;const selected=current.quickStarts.includes(id);const quickStarts=selected?current.quickStarts.filter((value)=>value!==id):[...current.quickStarts,id];return quickStarts.length?{...current,quickStarts}:current});
  const saveHomeAi=async()=>{if(!homeAiForm)return;setHomeAiBusy(true);try{await adminMutate('/api/admin/runtime/home-ai-builder','PATCH',homeAiForm);toast.success('Homepage AI Writer settings saved.');await refreshHomeAi()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setHomeAiBusy(false)}};
  const sandboxAction=async(item:AdminTerminalSandbox,action:'destroy'|'close-sessions')=>{const key=`${item.userId}:${action}`;setSandboxBusy(key);try{await adminMutate(`/api/admin/runtime/terminal-sandboxes/${encodeURIComponent(item.userId)}/${action}`,'POST',{});toast.success(action==='destroy'?`${item.containerName} destroyed.`:`Terminal sessions closed for ${item.containerName}.`);await refreshSandboxes()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setSandboxBusy('')}};
  return <>
    <AdminPageHead title="Runtime & AI Builder" copy="Control Cloud Terminal entitlements and the homepage AI project-creation entry point. Runtime changes apply to new/fresh allocations; AI Builder settings are reflected by the public configuration endpoint."/>
    {error?<ErrorNotice>{error}</ErrorNotice>:null}
    {data ? <section className="interactive-settings-card interactive-admin-terminal-limit-guide"><h2>Terminal safety and request limits</h2><p>Plan values are capped by deployment safety ceilings. PID/process limits protect the host from runaway processes; concurrent terminals limit simultaneous sessions; creations/hour counts only new Docker sandbox allocations in a rolling 60-minute window.</p><div className="interactive-stat-line interactive-stat-line-compact"><span>Hard PID/process ceiling</span><strong>{data.hardLimits.pidLimit}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Hard concurrent terminal ceiling</span><strong>{data.hardLimits.maxConcurrent}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Hard creations/hour ceiling</span><strong>{data.hardLimits.creationsPerHour}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Authenticated request limiter</span><strong>{data.requestLimits.authenticatedPerMinute === 0 ? 'Disabled' : `${data.requestLimits.authenticatedPerMinute}/min`}</strong></div><div className="interactive-stat-line interactive-stat-line-compact"><span>Guest request limiter</span><strong>{data.requestLimits.guestPerMinute === 0 ? 'Disabled' : `${data.requestLimits.guestPerMinute}/min`}</strong></div><p className="interactive-muted">Set LEARNITIVE_TERMINAL_MUTATION_RATE_PER_MINUTE=0 to disable the authenticated per-minute request limiter. Keep finite guest and deployment hard ceilings on public installations.</p></section> : null}
    <section className="interactive-settings-card">
      <div className="interactive-admin-template-editor-head"><div><span className="interactive-eyebrow">Lifecycle</span><h2>Active terminal environments</h2><p>Authenticated users keep one persistent Ubuntu environment with multiple PTYs. Guest environments are disposable and are removed after the last browser lease disconnects.</p></div><Button size="sm" variant="outline" onClick={()=>void refreshSandboxes()}>Refresh</Button></div>
      {sandboxError?<ErrorNotice>{sandboxError}</ErrorNotice>:null}
      {!sandboxData?<LoadingCard/>:sandboxData.sandboxes.length===0?<p className="interactive-muted">No Learnitive terminal containers are currently present.</p>:<div className="interactive-table-wrap"><table className="interactive-table"><thead><tr><th>Principal</th><th>Container</th><th>Type</th><th>Sessions</th><th>Sockets</th><th>Last activity</th><th>Expires</th><th>Status</th><th>Actions</th></tr></thead><tbody>{sandboxData.sandboxes.map((item)=><tr key={item.containerName}><td><code>{item.userId}</code></td><td><code>{item.containerName}</code></td><td>{item.type}</td><td>{item.activeSessions}</td><td>{item.connectedSockets}</td><td>{item.lastActivity?new Date(item.lastActivity).toLocaleString():'—'}</td><td>{new Date(item.expiresAt).toLocaleString()}</td><td>{item.status}</td><td><div className="flex flex-wrap gap-2"><Button size="sm" variant="outline" disabled={sandboxBusy===`${item.userId}:close-sessions`} onClick={()=>void sandboxAction(item,'close-sessions')}>Close PTYs</Button><Button size="sm" variant="destructive" disabled={sandboxBusy===`${item.userId}:destroy`} onClick={()=>void sandboxAction(item,'destroy')}>Destroy</Button></div></td></tr>)}</tbody></table></div>}
    </section>
    {!data?<LoadingCard/>:<div className="space-y-5">{data.plans.map((plan)=>{const form=forms[plan.planId]||plan.effective;return <section key={plan.planId} className="interactive-admin-template-editor">
      <div className="interactive-admin-template-editor-head"><div><span className="interactive-eyebrow">Terminal entitlement</span><h2>{ADMIN_TERMINAL_PLAN_LABELS[plan.planId]||plan.planId}</h2><p>Current lifetime: {adminDuration(form.maxLifetimeSeconds)} · network: {form.networkMode==='packages-git'?'Packages + Git':form.networkMode}</p></div>{plan.updatedAt?<small>Last changed {new Date(plan.updatedAt).toLocaleString()}</small>:<small>Using deployment defaults</small>}</div>
      <div className="interactive-admin-form-grid">
        <div><Label>Maximum lifetime (seconds)</Label><Input type="number" min={300} value={form.maxLifetimeSeconds} onChange={(e)=>update(plan.planId,'maxLifetimeSeconds',Number(e.currentTarget.value))}/><small>{adminDuration(form.maxLifetimeSeconds)}</small></div>
        <div><Label>Idle timeout (seconds)</Label><Input type="number" min={60} value={form.idleTimeoutSeconds} onChange={(e)=>update(plan.planId,'idleTimeoutSeconds',Number(e.currentTarget.value))}/><small>{adminDuration(form.idleTimeoutSeconds)}</small></div>
        <div><Label>Workspace recovery (seconds)</Label><Input type="number" min={1} value={form.workspaceRetentionSeconds} disabled={plan.planId==='guest'} onChange={(e)=>update(plan.planId,'workspaceRetentionSeconds',Number(e.currentTarget.value))}/><small>{plan.planId==='guest'?'Guests are disposable; browser disconnect cleanup overrides recovery retention.':adminDuration(form.workspaceRetentionSeconds)}</small></div>
        <div><Label>Temporary workspace bytes</Label><Input type="number" min={1_000_000} value={form.workspaceMaxBytes} onChange={(e)=>update(plan.planId,'workspaceMaxBytes',Number(e.currentTarget.value))}/><small>{adminBytes(form.workspaceMaxBytes)}</small></div>
        <div><Label>Upload/import bytes</Label><Input type="number" min={1_000_000} value={form.uploadMaxBytes} onChange={(e)=>update(plan.planId,'uploadMaxBytes',Number(e.currentTarget.value))}/><small>{adminBytes(form.uploadMaxBytes)}</small></div>
        <div><Label>Download bytes</Label><Input type="number" min={1_000_000} value={form.downloadMaxBytes} onChange={(e)=>update(plan.planId,'downloadMaxBytes',Number(e.currentTarget.value))}/><small>{adminBytes(form.downloadMaxBytes)}</small></div>
        <div><Label>Memory (MB)</Label><Input type="number" min={128} value={form.memoryMb} onChange={(e)=>update(plan.planId,'memoryMb',Number(e.currentTarget.value))}/></div>
        <div><Label>CPU limit</Label><Input type="number" min={0.1} step={0.1} value={form.cpuLimit} onChange={(e)=>update(plan.planId,'cpuLimit',Number(e.currentTarget.value))}/></div>
        <div><Label>PID/process limit</Label><Input type="number" min={32} value={form.pidLimit} onChange={(e)=>update(plan.planId,'pidLimit',Number(e.currentTarget.value))}/></div>
        <div><Label>Concurrent terminals</Label><Input type="number" min={1} value={form.maxConcurrent} onChange={(e)=>update(plan.planId,'maxConcurrent',Number(e.currentTarget.value))}/></div>
        <div><Label>Terminal creations/hour</Label><Input type="number" min={1} value={form.creationsPerHour} onChange={(e)=>update(plan.planId,'creationsPerHour',Number(e.currentTarget.value))}/></div>
        <div><Label>Network mode</Label><select value={form.networkMode} onChange={(e)=>update(plan.planId,'networkMode',e.currentTarget.value)}>{data.allowedNetworkModes.map((mode)=><option key={mode} value={mode}>{mode==='packages-git'?'Packages + Git':mode==='internet'?'Internet':'Off'}</option>)}</select><small>Packages + Git permits approved package registries and HTTPS Git destinations only.</small></div>
      </div>
      <div className="interactive-admin-template-actions"><Button disabled={busy===plan.planId} onClick={()=>void save(plan.planId)}>{busy===plan.planId?'Saving…':'Save terminal policy'}</Button></div>
    </section>})}</div>}

    <section className="interactive-admin-template-editor">
      <div className="interactive-admin-template-editor-head"><div><span className="interactive-eyebrow">Homepage</span><h2>AI Builder</h2><p>Controls the “Start Building” composer. AI credentials are never stored here; users configure Local AI, Ollama or BYOK in AI Settings.</p></div></div>
      {homeAiError?<ErrorNotice>{homeAiError}</ErrorNotice>:null}
      {!homeAiForm||!homeAiData?<LoadingCard/>:<>
        <div className="interactive-admin-form-grid">
          <label className="interactive-admin-checkbox"><input type="checkbox" checked={homeAiForm.enabled} onChange={(e)=>setHomeAiForm({...homeAiForm,enabled:e.currentTarget.checked})}/><span><strong>AI Writer enabled</strong><small>Show the homepage AI writing and project-creation section.</small></span></label>
          <label className="interactive-admin-checkbox"><input type="checkbox" checked={homeAiForm.anonymousVisible} onChange={(e)=>setHomeAiForm({...homeAiForm,anonymousVisible:e.currentTarget.checked})}/><span><strong>Visible before sign-in</strong><small>Guests can compose an idea, but Create still authenticates before any AI work.</small></span></label>
          <div><Label>Maximum prompt length</Label><Input type="number" min={200} max={16000} step={100} value={homeAiForm.maxPromptLength} onChange={(e)=>setHomeAiForm({...homeAiForm,maxPromptLength:Math.max(200,Math.min(16000,Number(e.currentTarget.value)||200))})}/><small>200–16,000 characters.</small></div>
        </div>
        <div className="interactive-admin-home-ai-quick-starts"><Label>Quick-start categories</Label><p className="interactive-muted">At least one category stays enabled. Every choice maps to a canonical supported project starter.</p><div>{homeAiData.allowedQuickStarts.map((id)=><label key={id}><input type="checkbox" checked={homeAiForm.quickStarts.includes(id)} onChange={()=>toggleHomeAiQuickStart(id)}/><span>{ADMIN_HOME_AI_QUICK_START_LABELS[id]||id}</span></label>)}</div></div>
        <div className="interactive-admin-template-actions"><Button disabled={homeAiBusy} onClick={()=>void saveHomeAi()}>{homeAiBusy?'Saving…':'Save AI Writer settings'}</Button></div>
      </>}
    </section>
  </>;
}

function AdminPager({page,pageSize,total,onPage}:{page:number;pageSize:number;total:number;onPage:(page:number)=>void}){const pages=Math.max(1,Math.ceil(total/pageSize));if(pages<=1)return null;return <nav className="interactive-admin-pager" aria-label="Pagination"><Button size="sm" variant="outline" disabled={page<=1} onClick={()=>onPage(page-1)}>Previous</Button><span>Page {page} of {pages} · {total} records</span><Button size="sm" variant="outline" disabled={page>=pages} onClick={()=>onPage(page+1)}>Next</Button></nav>}
function AdminListControls({search,onSearch,children}:{search:string;onSearch:(value:string)=>void;children?:React.ReactNode}){return <div className="interactive-admin-list-controls"><div className="interactive-admin-search-field"><Search className="size-4"/><Input type="search" value={search} onChange={(event)=>onSearch(event.currentTarget.value)} placeholder="Search" aria-label="Search records"/></div>{children}</div>}
function AdminUsers() {
  const [page,setPage]=React.useState(1);
  const [search,setSearch]=React.useState('');
  const [status,setStatus]=React.useState('');
  const [selected,setSelected]=React.useState<{item:Record<string,unknown>;projects:Array<Record<string,unknown>>;support:Array<Record<string,unknown>>}>();
  const path=`/api/admin/users?page=${page}&pageSize=25&search=${encodeURIComponent(search)}&status=${encodeURIComponent(status)}`;
  const {data,error,refresh}=useAdminData<{items:Array<Record<string,unknown>>;page:number;pageSize:number;total:number}>(path);
  const update=async(id:number,body:unknown)=>{await adminMutate(`/api/admin/users/${id}`,'PATCH',body);if(Number(selected?.item.id)===id){const detail=await adminGet<{item:Record<string,unknown>;projects:Array<Record<string,unknown>>;support:Array<Record<string,unknown>>}>(`/api/admin/users/${id}`);setSelected(detail)}await refresh()};
  const view=async(id:number)=>{try{setSelected(await adminGet<{item:Record<string,unknown>;projects:Array<Record<string,unknown>>;support:Array<Record<string,unknown>>}>(`/api/admin/users/${id}`))}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}};
  const changeSearch=(value:string)=>{setSearch(value);setPage(1)};
  const rows=data?.items||[];
  return <>
    <AdminPageHead title="Users" copy="Search, inspect account history, suspend accounts and review project and license status. Server-side admin authorization is mandatory."/>
    <AdminListControls search={search} onSearch={changeSearch}><select value={status} onChange={(event)=>{setStatus(event.currentTarget.value);setPage(1)}} aria-label="Filter users by status"><option value="">All active states</option><option value="active">Active</option><option value="suspended">Suspended</option></select></AdminListControls>
    {error?<ErrorNotice>{error}</ErrorNotice>:null}
    {rows.length?<div className="interactive-admin-table-wrap"><table className="interactive-admin-table"><thead><tr><th>User</th><th>Status</th><th>Projects</th><th>License</th><th>Support</th><th>Last login</th><th>Actions</th></tr></thead><tbody>{rows.map((row)=><tr key={String(row.id)}><td><div className="interactive-admin-account-cell">{row.avatar_url?<img className="interactive-admin-avatar" src={String(row.avatar_url)} alt="" referrerPolicy="no-referrer"/>:<CircleUserRound className="size-7"/>}<span><strong>{String(row.display_name||'User')}</strong><small>{String(row.email||'')}</small></span></div></td><td>{String(row.status||'')}</td><td>{String(row.project_count??0)}</td><td>{Boolean(row.pro)?String(row.license_type||'Pro'):'Free'}</td><td>{String(row.support_count??0)}</td><td>{row.last_login_at?String(row.last_login_at):'—'}</td><td className="interactive-admin-actions"><button type="button" onClick={()=>void view(Number(row.id))}>View</button><button type="button" onClick={()=>void update(Number(row.id),{status:row.status==='suspended'?'active':'suspended'})}>{row.status==='suspended'?'Reactivate':'Suspend'}</button></td></tr>)}</tbody></table></div>:<div className="interactive-empty-state"><p>No records.</p></div>}
    {data?<AdminPager page={data.page} pageSize={data.pageSize} total={data.total} onPage={setPage}/>:null}
    {selected?<article className="interactive-user-detail"><header><div><span className="interactive-eyebrow">Account #{String(selected.item.id)}</span><h2>{String(selected.item.display_name||selected.item.email||'User')}</h2><p>{String(selected.item.email||'')}</p></div><button type="button" className="interactive-icon-button" aria-label="Close user detail" onClick={()=>setSelected(undefined)}>×</button></header><dl><div><dt>Status</dt><dd>{String(selected.item.status||'')}</dd></div><div><dt>OAuth</dt><dd>{String(selected.item.provider||'—')}</dd></div><div><dt>License</dt><dd>{String(selected.item.license_type||'Free')}</dd></div><div><dt>Legacy online project records</dt><dd>{selected.projects.length}</dd></div><div><dt>Support tickets</dt><dd>{selected.support.length}</dd></div></dl><section><h3>Legacy explicit online resources</h3>{selected.projects.length?<ul>{selected.projects.map((project)=><li key={String(project.public_id)}><strong>{String(project.name)}</strong><span>{String(project.storage_type)} · {String(project.status)}</span></li>)}</ul>:<p className="interactive-muted">No legacy online project records.</p>}</section><section><h3>Support history</h3>{selected.support.length?<ul>{selected.support.map((ticket)=><li key={String(ticket.id)}><strong>{String(ticket.subject)}</strong><span>{String(ticket.status)} · {String(ticket.created_at)}</span></li>)}</ul>:<p className="interactive-muted">No support tickets.</p>}</section></article>:null}
  </>;
}

function AdminLicenses() {
  const [page,setPage]=React.useState(1);
  const [search,setSearch]=React.useState('');
  const [blocked,setBlocked]=React.useState('');
  const path=`/api/admin/licenses?page=${page}&pageSize=25&search=${encodeURIComponent(search)}&blocked=${encodeURIComponent(blocked)}`;
  const {data,error,refresh}=useAdminData<{items:Array<Record<string,unknown>>;page:number;pageSize:number;total:number}>(path);
  const update=async(id:number,body:unknown)=>{await adminMutate(`/api/admin/licenses/${id}`,'PATCH',body);await refresh()};
  const changeSearch=(value:string)=>{setSearch(value);setPage(1)};
  const rows=data?.items||[];
  return <>
    <AdminPageHead title="Licenses" copy="Search account licenses, review product type and Monthly expiry, and block entitlement when necessary. Changes are audited."/>
    <AdminListControls search={search} onSearch={changeSearch}><select value={blocked} onChange={(event)=>{setBlocked(event.currentTarget.value);setPage(1)}} aria-label="Filter licenses"><option value="">All licenses</option><option value="0">Unblocked</option><option value="1">Blocked</option></select></AdminListControls>
    {error?<ErrorNotice>{error}</ErrorNotice>:null}
    {rows.length?<div className="interactive-admin-table-wrap"><table className="interactive-admin-table"><thead><tr><th>License</th><th>Account</th><th>Provider</th><th>Type</th><th>Verified</th><th>Expires</th><th>Status</th><th>Actions</th></tr></thead><tbody>{rows.map((row)=><tr key={String(row.id)}><td><code>{String(row.license_key||'')}</code></td><td><strong>{String(row.display_name||'Unlinked')}</strong><br/><small>{String(row.email||'—')}</small></td><td>{String(row.provider||'—')}</td><td>{String(row.license_type||'Pro')}</td><td>{String(row.last_verified_at||row.verified_at||'—')}</td><td>{row.expires_at?String(row.expires_at):'Never'}</td><td>{Boolean(row.is_blocked)?'Blocked':Boolean(row.expired)?'Expired':'Active'}</td><td className="interactive-admin-actions"><button type="button" onClick={()=>void update(Number(row.id),{blocked:!Boolean(row.is_blocked)})}>{row.is_blocked?'Unblock':'Block'}</button></td></tr>)}</tbody></table></div>:<div className="interactive-empty-state"><p>No records.</p></div>}
    {data?<AdminPager page={data.page} pageSize={data.pageSize} total={data.total} onPage={setPage}/>:null}
  </>;
}

function AdminTemplatePreview({ slug }: { slug: string }) {
  const { data, error } = useAdminData<{ item: AdminTemplateRecord }>(`/api/admin/templates/${encodeURIComponent(slug)}`);
  const item = data?.item;
  if (error) return <><AdminPageHead title="Template preview" copy="Private Admin preview for draft and published templates."/><ErrorNotice>{error}</ErrorNotice></>;
  if (!item) return <><AdminPageHead title="Template preview" copy="Private Admin preview for draft and published templates."/><LoadingCard/></>;
  const previewUrl = `/api/templates/${encodeURIComponent(item.slug)}/assets/preview`;
  const provenance = item.provenance === 'official' ? 'Official source' : item.provenance === 'adapted' ? 'Adapted' : item.provenance === 'community' ? 'Community' : 'Learnitive-created';
  return <>
    <AdminPageHead title={item.name} copy={`Private template preview · ${item.status === 'published' ? 'Published' : item.status === 'archived' ? 'Archived' : 'Draft'}`} actions={<><a className="interactive-button interactive-button-secondary" href="/admin/templates">Back to templates</a>{item.status==='published'?<a className="interactive-button interactive-button-secondary" href={`/templates/${encodeURIComponent(item.slug)}`} target="_blank" rel="noreferrer">Open public page</a>:null}</>}/>
    <section className="interactive-template-detail interactive-admin-template-private-preview">
      <div className="interactive-template-preview">
        {item.previewResolved ? item.previewKind === 'pdf' ? <TemplatePdfPreview url={previewUrl} title={item.name}/> : <img src={previewUrl} alt={`${item.name} preview`}/> : <div className="interactive-template-placeholder"><FileText className="size-8"/><span>No preview is available yet.</span></div>}
      </div>
      <div className="interactive-template-detail-copy">
        <div className="interactive-card-meta"><span>{item.category||'Uncategorised'}</span><span className={item.tier==='pro'?'interactive-badge interactive-badge-pro':'interactive-badge'}>{item.tier==='pro'?'Pro':'Free'}</span><span>{provenance}</span><span>{item.status}</span></div>
        <h1 className="interactive-display">{item.name}</h1>
        {item.institution?<p className="interactive-template-institution">{item.institution}{item.degreeLevels?.length?` · ${item.degreeLevels.join(', ')}`:''}</p>:null}
        <p>{item.description||'No description has been added yet.'}</p>{item.longDescription?<p>{item.longDescription}</p>:null}
        <dl className="interactive-template-facts">
          <div><dt>Entry file</dt><dd><code>{item.entryFile||'—'}</code></dd></div><div><dt>Version</dt><dd>{item.version||'1.0.0'}</dd></div><div><dt>Author</dt><dd>{item.originalAuthor||item.author||'Learnitive'}</dd></div>
          {item.compiler?<div><dt>Compiler</dt><dd>{item.compiler}</dd></div>:null}{item.bibliographyEngine?<div><dt>Bibliography</dt><dd>{item.bibliographyEngine}</dd></div>:null}{item.documentClass?<div><dt>Document class</dt><dd><code>{item.documentClass}</code></dd></div>:null}{item.license?<div><dt>Licence</dt><dd>{item.license}</dd></div>:null}{item.lastVerified?<div><dt>Last verified</dt><dd>{item.lastVerified}</dd></div>:null}
        </dl>
        {item.features?.length?<section className="interactive-template-features"><h2>Template features</h2><ul>{item.features.map((feature)=><li key={feature}><Check className="size-4"/> {feature}</li>)}</ul></section>:null}
        {item.requiredPackages?.length?<section className="interactive-template-packages"><h2>Required packages</h2><div className="interactive-tag-row">{item.requiredPackages.map((pkg)=><span key={pkg}>{pkg}</span>)}</div></section>:null}
        {item.tags?.length?<div className="interactive-tag-row">{item.tags.map((tag)=><span key={tag}>{tag}</span>)}</div>:null}
        <aside className="interactive-template-provenance-note"><strong>Private Admin preview</strong><p>Draft and archived templates are intentionally absent from the public template manifest. Publish this template to make <code>/templates/{item.slug}</code> publicly resolvable.</p></aside>
      </div>
    </section>
  </>;
}

function adminCsv(value: readonly string[] | undefined) { return (value || []).join(', '); }
function adminLines(value: readonly string[] | undefined) { return (value || []).join('\n'); }
function parseAdminList(value: string) { return [...new Set(value.split(/[\n,]/u).map((item) => item.trim()).filter(Boolean))]; }
const ADMIN_TEMPLATE_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/u;
const ADMIN_TEMPLATE_ZIP_MAX_BYTES = 64 * 1024 * 1024;
const ADMIN_TEMPLATE_USE_CASES = ['starter','portfolio','landing-page','dashboard','blog','documentation','api','cli','data-processing','algorithm','game','research-paper','thesis','academic-article','resume-cv','presentation','letter','assignment','report','example-demo'] as const;
function adminTemplateSlug(value: string) {
  return value.normalize('NFKD').replace(/[\u0300-\u036f]/gu, '').toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-+|-+$/gu, '').slice(0, 80).replace(/-+$/gu, '');
}
function adminTemplateCreateError(reason: unknown) {
  const code = reason instanceof SaasApiError ? reason.code : '';
  const messages: Record<string, string> = {
    template_already_exists: 'A template with this slug already exists. Choose a different slug.',
    invalid_slug: 'Use a slug containing only lowercase letters, numbers and hyphens (maximum 80 characters).',
    invalid_name: 'Enter a template name of 160 characters or fewer.',
    template_zip_required: 'Choose a ZIP file containing the template project.',
    template_zip_too_large: 'The ZIP is too large. Template ZIPs are limited to 64 MiB.',
    invalid_template_zip: 'The selected file is not a valid ZIP archive.',
    unsafe_zip_path: 'The ZIP contains an unsafe file path and was rejected.',
    template_entry_file_required: 'Choose an Entry file before publishing this template.',
    template_entry_file_not_found: 'The selected Entry file does not exist in the uploaded template.',
    template_symlink_not_allowed: 'The ZIP contains a symbolic link, which is not allowed.',
    template_executable_not_allowed: 'The ZIP contains an executable-like file, which is not allowed.',
    template_file_too_large: 'One of the template files is larger than the 25 MiB per-file limit.',
    template_too_many_files: 'The template contains more than 5,000 files.',
    template_unpacked_too_large: 'The unpacked template is larger than the 150 MiB limit.',
    unsupported_template_entry: 'The ZIP contains an unsupported filesystem entry.',
    template_zip_tool_unavailable: 'The server ZIP extraction tool is currently unavailable.',
    invalid_template_metadata: 'The ZIP contains invalid template metadata.',
    invalid_project_mode: 'Choose one of the supported Language / Framework modes.',
    template_validation_required: 'Validate the current template revision successfully before publishing it.',
  };
  const friendly = code ? messages[code] : undefined;
  if (friendly) return friendly;
  return reason instanceof Error ? reason.message : String(reason);
}

function AdminTemplates() {
  const { data, error, refresh } = useAdminData<{ items: AdminTemplateRecord[]; institutions: AdminInstitutionRecord[]; categories: AdminTemplateCategoryRecord[] }>('/api/admin/templates');
  const [search, setSearch] = React.useState('');
  const [tierFilter, setTierFilter] = React.useState('');
  const [provenanceFilter, setProvenanceFilter] = React.useState('');
  const [sortKey, setSortKey] = React.useState<'name'|'tier'|'status'|'category'|'institution'|'createdAt'|'modifiedAt'>('createdAt');
  const [sortDirection, setSortDirection] = React.useState<'asc'|'desc'>('desc');
  const [selectedSlug, setSelectedSlug] = React.useState('');
  const [form, setForm] = React.useState<AdminTemplateRecord>();
  const [busy, setBusy] = React.useState(false);
  const [createOpen, setCreateOpen] = React.useState(false);
  const [createName, setCreateName] = React.useState('');
  const [createSlug, setCreateSlug] = React.useState('');
  const [createSlugEdited, setCreateSlugEdited] = React.useState(false);
  const [createTier, setCreateTier] = React.useState<'free'|'pro'>('free');
  const [createProjectModeId, setCreateProjectModeId] = React.useState('html');
  const [createUseCaseId, setCreateUseCaseId] = React.useState('starter');
  const [createFile, setCreateFile] = React.useState<File>();
  const [createError, setCreateError] = React.useState('');
  const [uploadTier, setUploadTier] = React.useState<'free'|'pro'>('free');
  const [uploadSlug, setUploadSlug] = React.useState('');
  const [checked, setChecked] = React.useState<Set<string>>(new Set());
  const [bulkInstitution, setBulkInstitution] = React.useState('');
  const [bulkCategory, setBulkCategory] = React.useState('');
  const [validation, setValidation] = React.useState<{ valid:boolean; structurallyValid:boolean; validationStatus:string; sourceRevision:string; errors:string[]; warnings:string[]; files:number; totalBytes:number; detected:{entryFile:string;projectModeId:string;projectType:string;primaryLanguage:string;framework?:string|null;documentClass:string;packages:string[];bibliographyResources:string[];bibliographyFiles:string[]} }>();
  const createFileRef = React.useRef<HTMLInputElement>(null);
  const uploadRef = React.useRef<HTMLInputElement>(null);
  const replaceRef = React.useRef<HTMLInputElement>(null);
  const previewRef = React.useRef<HTMLInputElement>(null);
  const thumbnailRef = React.useRef<HTMLInputElement>(null);
  const editorRef = React.useRef<HTMLElement>(null);

  React.useEffect(() => {
    if (!data) return;
    const selected = data.items.find((item) => item.slug === selectedSlug);
    if (selected) setForm(structuredClone(selected));
    else if (selectedSlug) { setSelectedSlug(''); setForm(undefined); }
  }, [data, selectedSlug]);

  React.useEffect(() => { setValidation(undefined); }, [selectedSlug]);

  const institutionLabel = (item: AdminTemplateRecord) => (data?.institutions||[]).find((institution)=>institution.id===item.institution)?.displayName || item.institution || '';
  const rows = (data?.items || []).filter((item) => {
    const needle = search.trim().toLowerCase();
    return (!tierFilter || item.tier === tierFilter)
      && (!provenanceFilter || (item.provenance || 'learnitive') === provenanceFilter)
      && (!needle || `${item.name} ${item.id || item.slug} ${item.slug} ${item.primaryLanguage || ''} ${item.framework || ''} ${item.useCaseId || ''} ${item.category || ''} ${item.institution || ''} ${item.provenance || ''} ${(item.tags || []).join(' ')}`.toLowerCase().includes(needle));
  }).sort((a,b)=>{
    const dateValue=(value?:string)=>{const parsed=value?Date.parse(value):0;return Number.isFinite(parsed)?parsed:0};
    const textValue=(item:AdminTemplateRecord)=>sortKey==='name'?`${item.name} ${item.slug}`:sortKey==='tier'?item.tier:sortKey==='status'?item.status:sortKey==='category'?item.category||'':institutionLabel(item);
    const comparison = sortKey==='createdAt' ? dateValue(a.createdAt)-dateValue(b.createdAt) : sortKey==='modifiedAt' ? dateValue(a.modifiedAt)-dateValue(b.modifiedAt) : textValue(a).localeCompare(textValue(b),'en',{sensitivity:'base'});
    const directed = sortDirection==='asc'?comparison:-comparison;
    return directed || a.slug.localeCompare(b.slug,'en');
  });
  const changeSort=(next:typeof sortKey)=>{if(next===sortKey)setSortDirection((current)=>current==='asc'?'desc':'asc');else{setSortKey(next);setSortDirection(next==='createdAt'||next==='modifiedAt'?'desc':'asc')}};
  const sortAria=(key:typeof sortKey):'ascending'|'descending'|'none'=>sortKey===key?(sortDirection==='asc'?'ascending':'descending'):'none';
  const sortIndicator=(key:typeof sortKey)=>sortKey===key?(sortDirection==='asc'?'▲':'▼'):'↕';
  const formatAdminTemplateDate=(value?:string)=>{if(!value)return '—';const date=new Date(value);return Number.isNaN(date.getTime())?'—':date.toLocaleString()};
  const allVisibleChecked=rows.length>0&&rows.every((item)=>checked.has(item.slug));
  const toggleAllVisible=()=>setChecked((current)=>{const next=new Set(current);if(allVisibleChecked)for(const item of rows)next.delete(item.slug);else for(const item of rows)next.add(item.slug);return next});
  const counts = data?.items.reduce((acc, item) => { acc.total += 1; acc[item.tier] += 1; if (item.status === 'published') acc.published += 1; return acc; }, {total:0,free:0,pro:0,published:0}) || {total:0,free:0,pro:0,published:0};

  const patchForm = <K extends keyof AdminTemplateRecord>(key: K, value: AdminTemplateRecord[K]) => setForm((current) => current ? ({ ...current, [key]: value }) : current);
  const save = async () => {
    if (!form || busy) return;
    setBusy(true);
    try {
      const body = { ...form } as Record<string, unknown>;
      delete body.id; delete body.createdAt; delete body.modifiedAt; delete body.previewResolved; delete body.previewKind; delete body.previewAutomatic; delete body.schemaVersion; delete body.projectType; delete body.primaryLanguageId; delete body.primaryLanguage; delete body.frameworkId; delete body.framework; delete body.buildOutput; delete body.sourceType; delete body.validationStatus; delete body.validationRevision; delete body.validationTimestamp; delete body.validationError; delete body.validationRuntimeVersion;
      await adminMutate(`/api/admin/templates/${encodeURIComponent(selectedSlug)}`, 'PATCH', body);
      toast.success('Template metadata saved and assets synchronised.');
      setSelectedSlug(form.slug);
      await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  const sync = async () => {
    if (busy) return; setBusy(true);
    try {
      const result = await adminMutate<{total:number;free:number;pro:number}>('/api/admin/templates/sync','POST',{});
      toast.success(`Assets sync complete: ${result.total} templates (${result.free} Free, ${result.pro} Pro).`);
      await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  const openCreateTemplate = () => {
    if (busy) return;
    setCreateName('');
    setCreateSlug('');
    setCreateSlugEdited(false);
    setCreateTier('free');
    setCreateProjectModeId('html');
    setCreateUseCaseId('starter');
    setCreateFile(undefined);
    setCreateError('');
    if (createFileRef.current) createFileRef.current.value = '';
    setCreateOpen(true);
  };
  const changeCreateName = (value: string) => {
    setCreateName(value);
    if (!createSlugEdited) setCreateSlug(adminTemplateSlug(value));
    if (createError) setCreateError('');
  };
  const changeCreateSlug = (value: string) => {
    setCreateSlugEdited(true);
    setCreateSlug(adminTemplateSlug(value));
    if (createError) setCreateError('');
  };
  const chooseCreateFile = (file?: File) => {
    if (!file) return;
    if (!/\.zip$/iu.test(file.name)) {
      setCreateFile(undefined);
      setCreateError('Choose a .zip file containing the template project.');
      if (createFileRef.current) createFileRef.current.value = '';
      return;
    }
    if (file.size > ADMIN_TEMPLATE_ZIP_MAX_BYTES) {
      setCreateFile(undefined);
      setCreateError('The ZIP is too large. Template ZIPs are limited to 64 MiB.');
      if (createFileRef.current) createFileRef.current.value = '';
      return;
    }
    setCreateFile(file);
    setCreateError('');
  };
  const createTemplate = async (event: React.FormEvent) => {
    event.preventDefault();
    if (busy) return;
    const name = createName.trim();
    const slug = createSlug.trim();
    if (!name || name.length > 160) { setCreateError('Enter a template name of 160 characters or fewer.'); return; }
    if (!ADMIN_TEMPLATE_SLUG_RE.test(slug)) { setCreateError('Use a slug containing only lowercase letters, numbers and hyphens (maximum 80 characters).'); return; }
    if (!createFile) { setCreateError('Choose a ZIP file containing the template project.'); return; }
    setBusy(true);
    setCreateError('');
    try {
      const result = await adminUploadTemplate(createFile, createTier, slug, name, createProjectModeId, createUseCaseId);
      setCreateOpen(false);
      setSearch('');
      setTierFilter('');
      await refresh();
      setForm(structuredClone(result.item));
      setSelectedSlug(result.item.slug);
      toast.success('Template created as draft. Review its metadata and validation before publishing.');
      window.requestAnimationFrame(() => editorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
    } catch (reason) {
      setCreateError(adminTemplateCreateError(reason));
    } finally {
      setBusy(false);
    }
  };
  const uploadFiles = async (files: FileList | null) => {
    if (!files?.length || busy) return;
    setBusy(true);
    try {
      let index = 0;
      for (const file of Array.from(files)) {
        await adminUploadTemplate(file, uploadTier, files.length === 1 ? uploadSlug.trim() || undefined : undefined);
        index += 1;
        toast.success(`Imported ${index}/${files.length}: ${file.name}`);
      }
      setUploadSlug('');
      await refresh();
      toast.success('ZIP import finished. New templates are drafts until you review and publish them.');
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); if (uploadRef.current) uploadRef.current.value = ''; }
  };
  const replaceZip = async (files: FileList | null) => {
    const file = files?.[0]; if (!file || !form || busy) return;
    setBusy(true);
    try { await adminReplaceTemplate(selectedSlug, file); toast.success('Template ZIP replaced safely.'); await refresh(); }
    catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); if (replaceRef.current) replaceRef.current.value = ''; }
  };
  const uploadPreview = async (file?: File) => {
    if (!file || !selectedSlug || busy) return;
    setBusy(true);
    try {
      const result = await adminUploadTemplatePreview(selectedSlug, file);
      toast.success(`Template ${result.kind === 'pdf' ? 'PDF' : 'image'} preview uploaded.`);
      await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); if (previewRef.current) previewRef.current.value = ''; }
  };
  const clearPreview = async () => {
    if (!selectedSlug || busy) return;
    setBusy(true);
    try {
      const result = await adminClearTemplatePreview(selectedSlug);
      toast.success(result.item.previewAutomatic ? `Uploaded preview removed. Automatic ${result.item.previewResolved} preview is active.` : 'Uploaded preview removed.');
      await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  const uploadThumbnail = async (file?: File) => {
    if (!file || !selectedSlug || busy) return;
    setBusy(true);
    try {
      const result = await adminUploadTemplateThumbnail(selectedSlug, file);
      toast.success(`Template thumbnail uploaded (${Math.max(1, Math.round(result.bytes / 1024))} KiB).`);
      await refresh();
    } catch (reason) {
      const code = reason instanceof SaasApiError ? reason.code : '';
      const message = code === 'template_thumbnail_too_large'
        ? 'The thumbnail is too large. Use a PNG, JPEG or WebP up to 5 MB.'
        : code === 'invalid_template_thumbnail' || code === 'template_thumbnail_type_mismatch'
          ? 'Choose a valid PNG, JPEG or WebP thumbnail.'
          : reason instanceof Error ? reason.message : String(reason);
      toast.error(message);
    } finally { setBusy(false); if (thumbnailRef.current) thumbnailRef.current.value = ''; }
  };
  const clearThumbnail = async () => {
    if (!selectedSlug || busy) return;
    setBusy(true);
    try {
      await adminClearTemplateThumbnail(selectedSlug);
      toast.success('Custom thumbnail removed. The default A4 thumbnail is now active.');
      await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };

  const validateSelected = async () => {
    if (!selectedSlug || busy) return;
    setBusy(true);
    try {
      const result = await adminMutate<{validation:NonNullable<typeof validation>}>(`/api/admin/templates/${encodeURIComponent(selectedSlug)}/validate`, 'POST', {});
      setValidation(result.validation);
      if (result.validation.valid) toast.success(`Template validated in the trusted runtime${result.validation.warnings.length ? ` with ${result.validation.warnings.length} warning(s)` : ''}.`);
      else toast.error(result.validation.errors[0] || 'Trusted runtime validation failed.');
    } catch (reason) {
      const message = reason instanceof Error ? reason.message : String(reason);
      toast.error(message);
      setValidation(undefined);
    } finally { setBusy(false); }
  };
  const bulkPatch = async (patch: Record<string, unknown>) => {
    if (!checked.size || busy) return;
    setBusy(true);
    try {
      for (const slug of checked) await adminMutate(`/api/admin/templates/${encodeURIComponent(slug)}`,'PATCH',patch);
      toast.success(`Updated ${checked.size} templates.`); setChecked(new Set()); await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  const deleteAdminTemplates = async (slugs: readonly string[]) => {
    const targets=[...new Set(slugs)].filter(Boolean);
    if(!targets.length||busy)return;
    const firstTarget=targets[0]??'';
    const label=targets.length===1?(data?.items||[]).find((item)=>item.slug===firstTarget)?.name||firstTarget:`${targets.length} templates`;
    if(!(await requestPageConfirm({title:targets.length===1?'Delete template?':'Delete selected templates?',description:`Permanently delete ${label} and its template files. This cannot be undone. Learnitive will automatically synchronise the published template catalog after deletion.`,confirmText:targets.length===1?'Delete template':`Delete ${targets.length} templates`,tone:'destructive'})))return;
    setBusy(true);
    try{
      const result=await adminMutate<{deleted:Array<{slug:string;tier:'free'|'pro';name:string}>;sync:{total:number;free:number;pro:number;contentHash:string}}>('/api/admin/templates/delete','POST',{slugs:targets});
      const removed=new Set(result.deleted.map((item)=>item.slug));
      setChecked((current)=>new Set([...current].filter((slug)=>!removed.has(slug))));
      if(removed.has(selectedSlug)){setSelectedSlug('');setForm(undefined)}
      toast.success(`Deleted ${result.deleted.length} ${result.deleted.length===1?'template':'templates'} and synchronised ${result.sync.total} published templates.`);
      await refresh();
    }catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}
    finally{setBusy(false)}
  };
  const archiveSelected=async()=>{if(!selectedSlug||busy)return;setBusy(true);try{await adminMutate(`/api/admin/templates/${encodeURIComponent(selectedSlug)}`,'PATCH',{status:'archived'});toast.success('Template archived and assets synchronised.');await refresh()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setBusy(false)}};
  const toggleChecked = (slug:string) => setChecked((current) => { const next=new Set(current); if(next.has(slug))next.delete(slug);else next.add(slug);return next; });

  return <>
    <AdminPageHead title="Templates" copy="Create and manage filesystem-backed Learnitive templates. New templates are created from ZIP projects as drafts, then reviewed, validated and published." actions={<Button disabled={busy} onClick={openCreateTemplate}><Plus className="size-4"/> Create template</Button>}/>
    <Dialog open={createOpen} onOpenChange={(open)=>{if(!busy)setCreateOpen(open)}}>
      <DialogContent className="sm:max-w-lg interactive-admin-create-template-dialog">
        <form onSubmit={(event)=>void createTemplate(event)} className="interactive-admin-create-template-form">
          <DialogHeader><DialogTitle>Create template</DialogTitle><DialogDescription>Choose the trusted Language / Framework mode, upload the source ZIP, then validate it in the same isolated runtime users will run.</DialogDescription></DialogHeader>
          <div className="interactive-admin-create-template-fields">
            <div><Label htmlFor="template-create-name">Template name</Label><Input id="template-create-name" autoFocus required maxLength={160} value={createName} onChange={(event)=>changeCreateName(event.currentTarget.value)} placeholder="UNSW PhD Thesis"/></div>
            <div><Label htmlFor="template-create-slug">Slug</Label><Input id="template-create-slug" required maxLength={80} value={createSlug} onChange={(event)=>changeCreateSlug(event.currentTarget.value)} placeholder="unsw-phd-thesis" aria-describedby="template-create-slug-help"/><small id="template-create-slug-help">Lowercase letters, numbers and hyphens. Generated from the name until you edit it.</small></div>
            <div><Label htmlFor="template-create-tier">Access</Label><select id="template-create-tier" value={createTier} onChange={(event)=>{setCreateTier(event.currentTarget.value as 'free'|'pro');if(createError)setCreateError('')}}><option value="free">Free</option><option value="pro">Pro</option></select></div>
            <div><Label htmlFor="template-create-mode">Language / Framework Mode</Label><select id="template-create-mode" value={createProjectModeId} onChange={(event)=>{setCreateProjectModeId(event.currentTarget.value);if(createError)setCreateError('')}}>{PROJECT_MODE_CATALOG.map((mode)=><option key={mode.id} value={mode.id}>{mode.label}</option>)}</select></div>
            <div><Label htmlFor="template-create-usecase">Use Case</Label><select id="template-create-usecase" value={createUseCaseId} onChange={(event)=>setCreateUseCaseId(event.currentTarget.value)}>{ADMIN_TEMPLATE_USE_CASES.map((value)=><option key={value} value={value}>{value.replaceAll('-', ' ')}</option>)}</select></div>
            <div className="interactive-admin-create-template-file">
              <div><Label>Template ZIP</Label><small>Maximum 64 MiB. Learnitive inspects the archive safely; the selected trusted mode determines runtime behavior. If entry detection is ambiguous, choose the Entry file after upload.</small></div>
              <input ref={createFileRef} type="file" accept=".zip,application/zip" className="sr-only" onChange={(event)=>chooseCreateFile(event.currentTarget.files?.[0])}/>
              <Button type="button" variant="outline" onClick={()=>createFileRef.current?.click()} disabled={busy}><Upload className="size-4"/> Choose ZIP</Button>
              {createFile?<div className="interactive-admin-create-template-file-selected"><FileText className="size-4"/><span><strong>{createFile.name}</strong><small>{(createFile.size/1024/1024).toFixed(createFile.size >= 1024 * 1024 ? 1 : 2)} MiB</small></span></div>:null}
            </div>
            {createError?<ErrorNotice>{createError}</ErrorNotice>:null}
          </div>
          <DialogFooter><Button type="button" variant="outline" disabled={busy} onClick={()=>setCreateOpen(false)}>Cancel</Button><Button type="submit" disabled={busy||!createName.trim()||!ADMIN_TEMPLATE_SLUG_RE.test(createSlug)||!createFile}>{busy?<LoaderCircle className="size-4 animate-spin"/>:<Plus className="size-4"/>} Create template</Button></DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
    {error ? <ErrorNotice>{error}</ErrorNotice> : null}
    <div className="interactive-admin-metrics interactive-template-admin-metrics"><article><span>Total</span><strong>{counts.total}</strong></article><article><span>Free</span><strong>{counts.free}</strong></article><article><span>Pro</span><strong>{counts.pro}</strong></article><article><span>Published</span><strong>{counts.published}</strong></article></div>
    <section className="interactive-admin-template-toolbar" aria-label="Template maintenance and bulk import">
      <div><Label htmlFor="template-upload-tier">Bulk import tier</Label><select id="template-upload-tier" value={uploadTier} onChange={(event)=>setUploadTier(event.currentTarget.value as 'free'|'pro')}><option value="free">Free</option><option value="pro">Pro</option></select></div>
      <div><Label htmlFor="template-upload-slug">Optional slug (one ZIP)</Label><Input id="template-upload-slug" value={uploadSlug} onChange={(event)=>setUploadSlug(event.currentTarget.value)} placeholder="unsw-phd-thesis"/></div>
      <input ref={uploadRef} type="file" accept=".zip,application/zip" multiple className="sr-only" onChange={(event)=>void uploadFiles(event.currentTarget.files)}/>
      <Button variant="outline" disabled={busy} onClick={()=>uploadRef.current?.click()}><Upload className="size-4"/> Bulk import ZIPs</Button>
      <Button variant="outline" disabled={busy} onClick={()=>void sync()}>{busy ? <LoaderCircle className="size-4 animate-spin"/> : null} Sync Template Assets</Button>
    </section>
    <div className="interactive-admin-template-layout">
      <section>
        <AdminListControls search={search} onSearch={setSearch}><select value={tierFilter} onChange={(event)=>setTierFilter(event.currentTarget.value)} aria-label="Filter template tier"><option value="">All tiers</option><option value="free">Free</option><option value="pro">Pro</option></select><select value={provenanceFilter} onChange={(event)=>setProvenanceFilter(event.currentTarget.value)} aria-label="Filter template provenance"><option value="">All provenance</option><option value="learnitive">Learnitive</option><option value="official">Official</option><option value="adapted">Adapted</option><option value="community">Community</option></select></AdminListControls>
        {checked.size ? <div className="interactive-admin-bulk-bar"><strong>{checked.size} selected</strong><select value={bulkInstitution} onChange={(event)=>setBulkInstitution(event.currentTarget.value)}><option value="">Choose institution…</option><option value="__none__">No institution</option>{(data?.institutions||[]).map((item)=><option key={item.id} value={item.id}>{item.displayName}</option>)}</select><Button size="sm" variant="outline" onClick={()=>void bulkPatch({institution:bulkInstitution==='__none__'?'':bulkInstitution})} disabled={!bulkInstitution}>Set institution</Button><select value={bulkCategory} onChange={(event)=>setBulkCategory(event.currentTarget.value)}><option value="">Choose category…</option><option value="__none__">Uncategorised</option>{(data?.categories||[]).filter((item)=>item.status!=='archived').map((item)=><option key={item.id} value={item.id}>{item.name}</option>)}</select><Button size="sm" variant="outline" onClick={()=>{const category=(data?.categories||[]).find((item)=>item.id===bulkCategory);void bulkPatch({categoryId:bulkCategory==='__none__'?'':bulkCategory,category:category?.name||''})}} disabled={!bulkCategory}>Set category</Button><Button size="sm" variant="outline" onClick={()=>void bulkPatch({tier:'free'})}>Set Free</Button><Button size="sm" variant="outline" onClick={()=>void bulkPatch({tier:'pro'})}>Set Pro</Button><Button size="sm" variant="destructive" onClick={()=>void deleteAdminTemplates([...checked])}><Trash2 className="size-4"/> Delete selected</Button></div> : null}
        <div className="interactive-admin-table-wrap"><table className="interactive-admin-table interactive-admin-template-table"><thead><tr><th><input type="checkbox" aria-label="Select all visible templates" checked={allVisibleChecked} onChange={toggleAllVisible}/></th><th aria-sort={sortAria('name')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('name')}>Template <span aria-hidden="true">{sortIndicator('name')}</span></button></th><th>ID</th><th>Provenance</th><th aria-sort={sortAria('tier')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('tier')}>Tier <span aria-hidden="true">{sortIndicator('tier')}</span></button></th><th aria-sort={sortAria('status')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('status')}>Status <span aria-hidden="true">{sortIndicator('status')}</span></button></th><th>Technology</th><th>Use Case / Validation</th><th aria-sort={sortAria('createdAt')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('createdAt')}>Created <span aria-hidden="true">{sortIndicator('createdAt')}</span></button></th><th aria-sort={sortAria('modifiedAt')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('modifiedAt')}>Updated <span aria-hidden="true">{sortIndicator('modifiedAt')}</span></button></th><th/></tr></thead><tbody>{rows.map((item)=><tr key={item.slug} data-selected={selectedSlug===item.slug}><td><input type="checkbox" aria-label={`Select ${item.name}`} checked={checked.has(item.slug)} onChange={()=>toggleChecked(item.slug)}/></td><td><strong>{item.name}</strong></td><td><code>{item.id || item.slug}</code></td><td><code>{item.provenance || 'learnitive'}</code></td><td>{item.tier}</td><td>{item.status}</td><td><strong>{item.primaryLanguage}</strong>{item.framework?<><br/><small>{item.framework}</small></>:null}</td><td>{item.useCaseId?.replaceAll('-', ' ')||'—'}<br/><small>{item.validationStatus}</small></td><td>{formatAdminTemplateDate(item.createdAt)}</td><td>{formatAdminTemplateDate(item.modifiedAt)}</td><td><button type="button" className="interactive-icon-button" aria-label={`Edit ${item.name}`} title="Edit template" onClick={()=>setSelectedSlug(item.slug)}><Pencil className="size-4" aria-hidden="true"/></button></td></tr>)}</tbody></table></div>
      </section>
      <section ref={editorRef} className="interactive-admin-template-editor">
        {form ? <>
          <div className="interactive-admin-template-editor-head"><div><span className="interactive-eyebrow">Template editor</span><h2>{form.name}</h2><code>{form.id || selectedSlug} · {form.provenance || 'learnitive'}</code><small>{form.tier}/{selectedSlug}/template.json</small></div><div className="interactive-admin-template-editor-links"><a className="interactive-button interactive-button-secondary interactive-button-sm" href={`/admin/templates/preview/${encodeURIComponent(selectedSlug)}`} target="_blank" rel="noreferrer">Preview template</a>{form.status==='published'?<a className="interactive-button interactive-button-secondary interactive-button-sm" href={`/templates/${encodeURIComponent(selectedSlug)}`} target="_blank" rel="noreferrer">Open public page</a>:null}</div></div>
          <div className="interactive-admin-form-grid">
            <div><Label>Title</Label><Input value={form.name||''} onChange={(e)=>patchForm('name',e.currentTarget.value)}/></div>
            <div><Label>Slug</Label><Input value={form.slug||''} onChange={(e)=>patchForm('slug',e.currentTarget.value)}/></div>
            <div><Label>Access</Label><select value={form.tier} onChange={(e)=>patchForm('tier',e.currentTarget.value as 'free'|'pro')}><option value="free">Free</option><option value="pro">Pro</option></select></div>
            <div><Label>Status</Label><select value={form.status} onChange={(e)=>patchForm('status',e.currentTarget.value as AdminTemplateRecord['status'])}><option value="draft">Draft</option><option value="published" disabled={form.validationStatus!=='valid'}>Published{form.validationStatus!=='valid'?' (validate first)':''}</option><option value="archived">Archived</option></select></div>
            <div><Label>Language / Framework Mode</Label><select value={form.projectModeId||'latex'} onChange={(e)=>{const mode=projectModeById(e.currentTarget.value);if(!mode)return;const language=projectLanguageById(mode.primaryLanguageId);setForm((current)=>current?{...current,projectModeId:mode.id,projectType:mode.projectType,primaryLanguageId:mode.primaryLanguageId,primaryLanguage:language?.label||mode.primaryLanguageId,frameworkId:mode.frameworkId||undefined,framework:mode.frameworkId==='react-vite'?'React + Vite':mode.frameworkId==='vite'?'Vite':mode.frameworkId==='vue-vite'?'Vue + Vite':mode.frameworkId==='svelte-vite'?'Svelte + Vite':mode.frameworkId==='node'?'Node.js':mode.frameworkId==='vanilla-web'?'Vanilla Web':undefined,entryFile:mode.defaultEntryFile}:current)}}>{PROJECT_MODE_CATALOG.map((mode)=><option key={mode.id} value={mode.id}>{mode.label}</option>)}</select></div>
            <div><Label>Language</Label><Input readOnly value={form.primaryLanguage||'—'}/></div>
            <div><Label>Framework</Label><Input readOnly value={form.framework||'None'}/></div>
            <div><Label>Project Type</Label><Input readOnly value={form.projectType||'—'}/></div>
            <div><Label>Use Case</Label><select value={form.useCaseId||'starter'} onChange={(e)=>patchForm('useCaseId',e.currentTarget.value)}>{ADMIN_TEMPLATE_USE_CASES.map((value)=><option key={value} value={value}>{value.replaceAll('-', ' ')}</option>)}</select></div>
            <div><Label>Runtime validation</Label><Input readOnly value={form.validationStatus||'draft'}/><small>{form.validationTimestamp?`Checked ${new Date(form.validationTimestamp).toLocaleString()}`:'Not validated in the trusted runtime yet.'}</small></div>
            <div className="interactive-admin-form-span"><strong>Legacy LaTeX / academic discovery metadata</strong><p className="interactive-muted">Institution and document category are preserved for academic LaTeX templates. They do not control runtime behavior.</p></div>
            <div><Label>Legacy category</Label><select value={form.categoryId||''} onChange={(e)=>{const id=e.currentTarget.value;const category=(data?.categories||[]).find((item)=>item.id===id);setForm((current)=>current?({...current,categoryId:id,category:category?.name||''}):current)}}><option value="">Uncategorised</option>{(data?.categories||[]).filter((item)=>item.status!=='archived'||item.id===form.categoryId).map((item)=><option key={item.id} value={item.id}>{item.name}{item.status!=='published'?` (${item.status})`:''}</option>)}</select><a className="interactive-text-link" href="/admin/template-categories">Manage categories →</a></div>
            <div><Label>Academic institution</Label><select value={form.institution||''} onChange={(e)=>{const id=e.currentTarget.value;const inst=(data?.institutions||[]).find((item)=>item.id===id);setForm((current)=>current?{...current,institution:id,country:current.country||inst?.country||'',countryCode:current.countryCode||inst?.countryCode||'',region:current.region||inst?.region||''}:current)}}><option value="">None</option>{(data?.institutions||[]).map((item)=><option key={item.id} value={item.id}>{item.displayName}</option>)}</select><a className="interactive-text-link" href="/admin/institutions">Manage institutions →</a></div>
            <div><Label>Country</Label><Input value={form.country||''} onChange={(e)=>patchForm('country',e.currentTarget.value)}/></div>
            <div><Label>Country code</Label><Input value={form.countryCode||''} onChange={(e)=>patchForm('countryCode',e.currentTarget.value.toUpperCase())}/></div>
            <div><Label>State / region</Label><Input value={form.region||''} onChange={(e)=>patchForm('region',e.currentTarget.value)}/></div>
            <div><Label>Degree levels</Label><Input value={adminCsv(form.degreeLevels)} onChange={(e)=>patchForm('degreeLevels',parseAdminList(e.currentTarget.value))} placeholder="PhD, Masters"/></div>
            <div><Label>Faculty / school</Label><Input value={form.faculty||''} onChange={(e)=>patchForm('faculty',e.currentTarget.value)}/></div>
            <div><Label>Department</Label><Input value={form.department||''} onChange={(e)=>patchForm('department',e.currentTarget.value)}/></div>
            <div><Label>Journal</Label><Input value={form.journal||''} onChange={(e)=>patchForm('journal',e.currentTarget.value)}/></div>
            <div><Label>Publisher</Label><Input value={form.publisher||''} onChange={(e)=>patchForm('publisher',e.currentTarget.value)}/></div>
            <div><Label>Conference</Label><Input value={form.conference||''} onChange={(e)=>patchForm('conference',e.currentTarget.value)}/></div>
            <div><Label>Provenance</Label><select value={form.provenance||'learnitive'} onChange={(e)=>patchForm('provenance',e.currentTarget.value as AdminTemplateRecord['provenance'])}><option value="learnitive">Learnitive-created</option><option value="official">Official source</option><option value="adapted">Adapted</option><option value="community">Community</option></select></div>
            <div><Label>Original author</Label><Input value={form.originalAuthor||''} onChange={(e)=>patchForm('originalAuthor',e.currentTarget.value)}/></div>
            <div><Label>Licence</Label><Input value={form.license||''} onChange={(e)=>patchForm('license',e.currentTarget.value)}/></div>
            <div className="interactive-admin-form-span"><Label>Source URL</Label><Input type="url" value={form.sourceUrl||''} onChange={(e)=>patchForm('sourceUrl',e.currentTarget.value)}/></div>
            <div><Label>Entry file</Label><Input value={form.entryFile||''} onChange={(e)=>patchForm('entryFile',e.currentTarget.value)}/></div>
            <div><Label>Compiler</Label><select value={form.compiler||''} onChange={(e)=>patchForm('compiler',e.currentTarget.value)}><option value="">Auto/unspecified</option><option value="pdflatex">pdfLaTeX</option><option value="xelatex">XeLaTeX</option><option value="lualatex">LuaLaTeX</option></select></div>
            <div><Label>Bibliography engine</Label><Input value={form.bibliographyEngine||''} onChange={(e)=>patchForm('bibliographyEngine',e.currentTarget.value)}/></div>
            <div><Label>Document class</Label><Input value={form.documentClass||''} onChange={(e)=>patchForm('documentClass',e.currentTarget.value)}/></div>
            <div><Label>Version</Label><Input value={form.version||''} onChange={(e)=>patchForm('version',e.currentTarget.value)}/></div>
            <div><Label>Last verified</Label><Input type="date" value={(form.lastVerified||'').slice(0,10)} onChange={(e)=>patchForm('lastVerified',e.currentTarget.value)}/></div>
            <div><Label>Thumbnail path (advanced)</Label><Input value={form.thumbnail||''} onChange={(e)=>patchForm('thumbnail',e.currentTarget.value)} placeholder="Optional — normally managed by the uploader below"/></div>
            <div><Label>Preview path</Label><Input value={form.preview||''} onChange={(e)=>patchForm('preview',e.currentTarget.value)} placeholder="Optional — template.pdf or main.pdf is detected automatically"/></div>
            <div className="interactive-admin-form-span interactive-admin-template-preview-upload interactive-admin-template-thumbnail-upload">
              <div className="interactive-admin-template-preview-upload-head"><div><Label>Catalog thumbnail</Label><p>Upload PNG, JPEG or WebP up to 5 MB. A4 portrait artwork is recommended. When no custom thumbnail is set, Learnitive uses the default A4 thumbnail automatically.</p></div><div className="interactive-admin-template-preview-actions"><input ref={thumbnailRef} type="file" accept="image/png,image/jpeg,image/webp" className="sr-only" onChange={(event)=>void uploadThumbnail(event.currentTarget.files?.[0])}/><Button type="button" size="sm" variant="outline" disabled={busy} onClick={()=>thumbnailRef.current?.click()}>{form.thumbnail ? 'Replace thumbnail' : 'Upload thumbnail'}</Button>{form.thumbnail ? <Button type="button" size="sm" variant="ghost" disabled={busy} onClick={()=>void clearThumbnail()}>Remove thumbnail</Button> : null}</div></div>
              <div className="interactive-admin-template-preview-media interactive-admin-template-thumbnail-media"><img src={form.thumbnail ? `/api/templates/${encodeURIComponent(selectedSlug)}/assets/thumbnail` : DEFAULT_TEMPLATE_THUMBNAIL_URL} alt={form.thumbnail ? `${form.name} catalog thumbnail` : 'Default Learnitive template thumbnail'} /><p>{form.thumbnail ? <>Custom thumbnail: <code>{form.thumbnail}</code></> : <>Default catalog thumbnail: <code>learnitive-template-default-thumbnail.webp</code></>}</p></div>
            </div>
            <div className="interactive-admin-form-span interactive-admin-template-preview-upload">
              <div className="interactive-admin-template-preview-upload-head"><div><Label>Template preview</Label><p>Upload PNG, JPEG, WebP or PDF (up to 20 MB). If no explicit preview is set, Learnitive automatically uses <code>template.pdf</code> or <code>main.pdf</code> when present.</p></div><div className="interactive-admin-template-preview-actions"><input ref={previewRef} type="file" accept="image/png,image/jpeg,image/webp,application/pdf,.pdf" className="sr-only" onChange={(event)=>void uploadPreview(event.currentTarget.files?.[0])}/><Button type="button" size="sm" variant="outline" disabled={busy} onClick={()=>previewRef.current?.click()}>{form.previewResolved ? 'Replace preview' : 'Upload preview'}</Button>{form.preview ? <Button type="button" size="sm" variant="ghost" disabled={busy} onClick={()=>void clearPreview()}>Remove uploaded preview</Button> : null}</div></div>
              {form.previewResolved ? <div className="interactive-admin-template-preview-media">{form.previewKind === 'pdf' ? <TemplatePdfPreview url={`/api/templates/${encodeURIComponent(selectedSlug)}/assets/preview`} title={form.name}/> : <img src={`/api/templates/${encodeURIComponent(selectedSlug)}/assets/preview`} alt={`${form.name} preview`} />}<p>{form.previewAutomatic ? <>Automatic fallback: <code>{form.previewResolved}</code></> : <>Explicit preview: <code>{form.previewResolved}</code></>}</p></div> : <div className="interactive-admin-template-preview-empty"><FileText className="size-6"/><span>No preview is available yet.</span></div>}
            </div>
            <div className="interactive-admin-form-span"><Label>Description</Label><textarea value={form.description||''} onChange={(e)=>patchForm('description',e.currentTarget.value)} rows={3}/></div>
            <div className="interactive-admin-form-span"><Label>Long description</Label><textarea value={form.longDescription||''} onChange={(e)=>patchForm('longDescription',e.currentTarget.value)} rows={5}/></div>
            <div className="interactive-admin-form-span"><Label>Features — one per line</Label><textarea value={adminLines(form.features)} onChange={(e)=>patchForm('features',parseAdminList(e.currentTarget.value))} rows={5}/></div>
            <div className="interactive-admin-form-span"><Label>Required packages</Label><textarea value={adminCsv(form.requiredPackages)} onChange={(e)=>patchForm('requiredPackages',parseAdminList(e.currentTarget.value))} rows={3}/></div>
            <div className="interactive-admin-form-span"><Label>Tags / searchable keywords</Label><textarea value={adminCsv(form.tags)} onChange={(e)=>patchForm('tags',parseAdminList(e.currentTarget.value))} rows={3}/></div>
            <div className="interactive-admin-form-span"><Label>Attribution</Label><textarea value={form.attribution||''} onChange={(e)=>patchForm('attribution',e.currentTarget.value)} rows={3}/></div>
            <div className="interactive-admin-form-span"><Label>SEO title</Label><Input value={form.seoTitle||''} onChange={(e)=>patchForm('seoTitle',e.currentTarget.value)}/></div>
            <div className="interactive-admin-form-span"><Label>SEO description</Label><textarea value={form.seoDescription||''} onChange={(e)=>patchForm('seoDescription',e.currentTarget.value)} rows={3}/></div>
            <label className="interactive-admin-check"><input type="checkbox" checked={Boolean(form.noIndex)} onChange={(e)=>patchForm('noIndex',e.currentTarget.checked)}/> Noindex this template page</label>
          </div>
          <div className="interactive-admin-template-actions"><Button disabled={busy} onClick={()=>void save()}>Save template</Button><Button variant="outline" disabled={busy} onClick={()=>void validateSelected()}>Validate Template</Button><input ref={replaceRef} type="file" accept=".zip,application/zip" className="sr-only" onChange={(event)=>void replaceZip(event.currentTarget.files)}/><Button variant="outline" disabled={busy} onClick={()=>replaceRef.current?.click()}>Replace ZIP</Button><Button variant="outline" disabled={busy} onClick={()=>void archiveSelected()}>Archive</Button><Button variant="destructive" disabled={busy} onClick={()=>void deleteAdminTemplates([selectedSlug])}><Trash2 className="size-4"/> Delete template</Button></div>{validation?<div className={`interactive-template-validation ${validation.valid?'is-valid':'is-invalid'}`}><strong>{validation.valid?'Runtime validation passed':validation.structurallyValid?'Runtime validation required/failed':'Template structure requires attention'}</strong><p>{validation.files} project files · {(validation.totalBytes/1024).toFixed(1)} KiB · entry {validation.detected.entryFile||'not detected'}{validation.detected.documentClass?` · class ${validation.detected.documentClass}`:''}</p>{validation.errors.length?<ul>{validation.errors.map((item)=><li key={item}>{item}</li>)}</ul>:null}{validation.warnings.length?<><span>Warnings</span><ul>{validation.warnings.map((item)=><li key={item}>{item}</li>)}</ul></>:null}</div>:null}
        </> : <div className="interactive-empty-state"><LayoutTemplate className="size-8"/><h2>Select a template</h2><p>Choose a template to manage its trusted project mode, discovery metadata, validation state, source archive and publication status.</p></div>}
      </section>
    </div>
  </>;
}

function AdminTemplateCategories() {
  const { data, error, refresh } = useAdminData<{ categories: AdminTemplateCategoryRecord[]; counts: Record<string, number> }>('/api/admin/template-categories');
  const blank = React.useCallback((): AdminTemplateCategoryRecord => ({ id:'', slug:'', name:'', description:'', icon:'FileText', sortOrder:0, status:'published', provenance:'learnitive', createdAt:'', updatedAt:'' }), []);
  const [form, setForm] = React.useState<AdminTemplateCategoryRecord>(() => blank());
  const [selected, setSelected] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [iconOpen, setIconOpen] = React.useState(false);
  const [slugTouched, setSlugTouched] = React.useState(false);
  const [search, setSearch] = React.useState('');
  const [provenanceFilter, setProvenanceFilter] = React.useState('');
  const [sortKey, setSortKey] = React.useState<'name'|'status'|'templates'|'sortOrder'|'createdAt'|'updatedAt'>('sortOrder');
  const [sortDirection, setSortDirection] = React.useState<'asc'|'desc'>('asc');
  const categoryNeedle = search.trim().toLowerCase();
  const rows = [...(data?.categories || [])].filter((item) => (!provenanceFilter || (item.provenance || 'learnitive') === provenanceFilter) && (!categoryNeedle || `${item.name} ${item.id} ${item.provenance || ''} ${item.description || ''}`.toLowerCase().includes(categoryNeedle))).sort((a,b) => {
    let comparison = 0;
    if (sortKey === 'templates') comparison = (data?.counts[a.id] || 0) - (data?.counts[b.id] || 0);
    else if (sortKey === 'sortOrder') comparison = a.sortOrder - b.sortOrder;
    else if (sortKey === 'createdAt' || sortKey === 'updatedAt') comparison = Date.parse(a[sortKey] || '') - Date.parse(b[sortKey] || '');
    else comparison = String(a[sortKey] || '').localeCompare(String(b[sortKey] || ''), 'en', { sensitivity:'base' });
    const directed = sortDirection === 'asc' ? comparison : -comparison;
    return directed || a.name.localeCompare(b.name, 'en');
  });
  const changeSort = (key: typeof sortKey) => { if (key === sortKey) setSortDirection((value)=>value==='asc'?'desc':'asc'); else { setSortKey(key); setSortDirection(key === 'createdAt' || key === 'updatedAt' ? 'desc' : 'asc'); } };
  const indicator = (key: typeof sortKey) => sortKey === key ? (sortDirection === 'asc' ? '▲' : '▼') : '↕';
  const ariaSort = (key: typeof sortKey): 'ascending'|'descending'|'none' => sortKey === key ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none';
  const edit = (item: AdminTemplateCategoryRecord) => { setSelected(item.id); setForm(structuredClone(item)); setSlugTouched(true); };
  const create = () => { setSelected(''); setForm(blank()); setSlugTouched(false); };
  const changeName = (name: string) => setForm((current) => { const generated = templateCategorySlug(name); return { ...current, name, ...(!selected && !slugTouched ? { id: generated, slug: generated } : {}) }; });
  const save = async () => {
    if (busy) return;
    const id = selected || form.id || templateCategorySlug(form.name);
    if (!id || !form.name.trim()) { toast.error('Category name and slug are required.'); return; }
    setBusy(true);
    try {
      const payload = { id, slug:id, name:form.name.trim(), description:form.description || '', icon:form.icon || '', sortOrder:Number(form.sortOrder)||0, status:form.status, provenance:form.provenance || 'learnitive' };
      const result = selected
        ? await adminMutate<{category:AdminTemplateCategoryRecord}>(`/api/admin/template-categories/${encodeURIComponent(selected)}`,'PATCH',{ name:payload.name, description:payload.description, icon:payload.icon, sortOrder:payload.sortOrder, status:payload.status, provenance:payload.provenance })
        : await adminMutate<{category:AdminTemplateCategoryRecord}>('/api/admin/template-categories','POST',payload);
      toast.success(selected ? 'Template category updated and assets synchronised.' : 'Template category created and assets synchronised.');
      setSelected(result.category.id); setForm(structuredClone(result.category)); setSlugTouched(true); await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  const remove = async () => {
    if (!selected || busy) return;
    const count = data?.counts[selected] || 0;
    if (!(await requestPageConfirm({ title:'Delete template category?', description: count ? `This category is assigned to ${count} template${count===1?'':'s'}. You must reassign them or explicitly move them to Uncategorised.` : 'This permanently removes the category. Templates are not deleted.', confirmText:'Continue', tone:'destructive' }))) return;
    let reassignTo: string | undefined;
    if (count) {
      const value = await requestPagePrompt({ title:'Reassign templates', description:'Enter another managed category ID, or leave blank to move the templates to Uncategorised.', label:'Replacement category ID', confirmText:'Delete category' });
      if (value === null) return;
      reassignTo = value.trim();
    }
    setBusy(true);
    try {
      await adminMutate(`/api/admin/template-categories/${encodeURIComponent(selected)}`,'DELETE',count ? { reassignTo } : {});
      toast.success(`Category deleted${count ? ` and ${count} template${count===1?'':'s'} reassigned` : ''}. Template assets synchronised.`);
      setSelected(''); setForm(blank()); setSlugTouched(false); await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  const formatDate = (value:string) => { const date = new Date(value); return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString(); };
  return <>
    <AdminPageHead title="Template Categories" copy="Manage the document taxonomy used by template filters and the Browse by document cards. Categories have stable IDs, public copy, ordering and a Lucide icon." actions={<Button onClick={create}><Plus className="size-4"/> Create category</Button>}/>
    {error ? <ErrorNotice>{error}</ErrorNotice> : null}
    <AdminListControls search={search} onSearch={setSearch}><select value={provenanceFilter} onChange={(event)=>setProvenanceFilter(event.currentTarget.value)} aria-label="Filter category provenance"><option value="">All provenance</option><option value="learnitive">Learnitive</option><option value="official">Official</option><option value="adapted">Adapted</option><option value="community">Community</option></select></AdminListControls>
    <div className="interactive-admin-template-layout interactive-admin-category-layout">
      <section className="interactive-admin-table-wrap"><table className="interactive-admin-table"><thead><tr>
        <th aria-sort={ariaSort('name')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('name')}>Category <span>{indicator('name')}</span></button></th>
        <th>ID</th><th>Provenance</th><th>Icon</th>
        <th aria-sort={ariaSort('status')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('status')}>Status <span>{indicator('status')}</span></button></th>
        <th aria-sort={ariaSort('templates')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('templates')}>Templates <span>{indicator('templates')}</span></button></th>
        <th aria-sort={ariaSort('sortOrder')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('sortOrder')}>Order <span>{indicator('sortOrder')}</span></button></th>
        <th aria-sort={ariaSort('createdAt')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('createdAt')}>Created <span>{indicator('createdAt')}</span></button></th>
        <th aria-sort={ariaSort('updatedAt')}><button type="button" className="interactive-admin-sort-button" onClick={()=>changeSort('updatedAt')}>Updated <span>{indicator('updatedAt')}</span></button></th><th/>
      </tr></thead><tbody>{rows.map((item)=><tr key={item.id} data-selected={selected===item.id}><td><strong>{item.name}</strong></td><td><code>{item.id}</code></td><td><code>{item.provenance || 'learnitive'}</code></td><td><span className="interactive-admin-category-icon-cell"><TemplateCategoryIcon name={item.icon} className="size-5"/> {item.icon || 'Default'}</span></td><td>{item.status}</td><td>{data?.counts[item.id]||0}</td><td>{item.sortOrder}</td><td>{formatDate(item.createdAt)}</td><td>{formatDate(item.updatedAt)}</td><td><button type="button" className="interactive-icon-button" aria-label={`Edit ${item.name}`} title="Edit category" onClick={()=>edit(item)}><Pencil className="size-4"/></button></td></tr>)}</tbody></table></section>
      <section className="interactive-admin-template-editor interactive-admin-category-editor">
        <div className="interactive-admin-template-editor-head"><div><span className="interactive-eyebrow">Category editor</span><h2>{selected ? form.name : 'Create category'}</h2><p>Stable IDs keep category URLs and template assignments intact when the display name changes.</p></div></div>
        <div className="interactive-admin-form-grid">
          <div><Label htmlFor="template-category-name">Name</Label><Input id="template-category-name" value={form.name} onChange={(event)=>changeName(event.currentTarget.value)} placeholder="Conference Papers"/></div>
          <div><Label htmlFor="template-category-id">Stable ID / slug</Label><Input id="template-category-id" value={form.id} disabled={Boolean(selected)} onChange={(event)=>{setSlugTouched(true);const value=templateCategorySlug(event.currentTarget.value);setForm((current)=>({...current,id:value,slug:value}))}} placeholder="conference-papers"/></div>
          <div><Label htmlFor="template-category-provenance">Provenance</Label><select id="template-category-provenance" value={form.provenance||'learnitive'} onChange={(event)=>{const provenance=event.currentTarget.value as AdminTemplateCategoryRecord['provenance'];setForm((current)=>({...current,provenance}))}}><option value="learnitive">Learnitive-created</option><option value="official">Official source</option><option value="adapted">Adapted</option><option value="community">Community</option></select></div>
          <div><Label htmlFor="template-category-status">Status</Label><select id="template-category-status" value={form.status} onChange={(event)=>{const status=event.currentTarget.value as AdminTemplateCategoryRecord['status'];setForm((current)=>({...current,status}))}}><option value="published">Published</option><option value="draft">Draft</option><option value="archived">Archived</option></select></div>
          <div><Label htmlFor="template-category-sort-order">Sort order</Label><Input id="template-category-sort-order" type="number" value={String(form.sortOrder)} onChange={(event)=>{const sortOrder=Number(event.currentTarget.value)||0;setForm((current)=>({...current,sortOrder}))}}/></div>
          <div className="interactive-admin-form-span"><Label htmlFor="template-category-description">Description</Label><textarea id="template-category-description" rows={4} value={form.description||''} onChange={(event)=>{const description=event.currentTarget.value;setForm((current)=>({...current,description}))}} placeholder="Explain what belongs in this document category."/></div>
          <div className="interactive-admin-form-span interactive-admin-category-icon-field"><div><Label>Lucide icon</Label><p>Choose visually from the full Lucide library. Only the canonical icon name is stored.</p></div><div className="interactive-admin-category-icon-choice"><span><TemplateCategoryIcon name={form.icon} className="size-6"/> {form.icon || 'Default FileText'}</span><Button type="button" variant="outline" onClick={()=>setIconOpen(true)}>Browse icons</Button></div></div>
        </div>
        <div className="interactive-admin-category-card-preview"><span className="interactive-eyebrow">Live public card preview</span><div className="interactive-template-category-card"><div className="interactive-template-category-card-copy"><span className="interactive-template-category-icon"><TemplateCategoryIcon name={form.icon} className="size-6"/></span><span>{selected ? `${data?.counts[selected]||0} templates` : '0 templates'}</span><h3>{form.name || 'Category name'}</h3><p>{form.description || 'Category description will appear here.'}</p></div><ArrowRight className="size-5"/></div></div>
        <div className="interactive-admin-template-actions"><Button disabled={busy} onClick={()=>void save()}>{selected?'Save category':'Create category'}</Button>{selected?<Button variant="destructive" disabled={busy} onClick={()=>void remove()}><Trash2 className="size-4"/> Delete category</Button>:null}</div>
      </section>
    </div>
    {iconOpen ? <React.Suspense fallback={<div className="interactive-category-icon-loading" role="status">Loading Lucide icon browser…</div>}><TemplateCategoryIconBrowser open value={form.icon} onSelect={(icon)=>setForm((current)=>({...current,icon}))} onClose={()=>setIconOpen(false)}/></React.Suspense> : null}
  </>;
}


function AdminLegalPages() {
  const {data,error,refresh}=useAdminData<{items:LegalPageRecord[]}>('/api/admin/legal');
  const [forms,setForms]=React.useState<Record<'privacy'|'terms',LegalPageRecord>>({privacy:{slug:'privacy',title:'',content_markdown:''},terms:{slug:'terms',title:'',content_markdown:''}});
  const [busy,setBusy]=React.useState('');
  React.useEffect(()=>{if(!data?.items)return;setForms((current)=>{const next={...current};for(const item of data.items)if(item.slug==='privacy'||item.slug==='terms')next[item.slug]=structuredClone(item);return next})},[data]);
  const patch=(slug:'privacy'|'terms',key:'title'|'content_markdown',value:string)=>setForms((current)=>({...current,[slug]:{...current[slug],[key]:value}}));
  const save=async(slug:'privacy'|'terms')=>{const form=forms[slug];if(busy||!form.title.trim())return;setBusy(slug);try{await adminMutate(`/api/admin/legal/${slug}`,'PATCH',{title:form.title,contentMarkdown:form.content_markdown||''});toast.success(`${form.title} page saved.`);await refresh()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setBusy('')}};
  return <>
    <AdminPageHead title="Legal pages" copy="Edit Privacy and Terms from the database. Changes publish immediately without a deployment." actions={<div className="interactive-admin-head-actions"><a className="interactive-button interactive-button-secondary" href="/privacy" target="_blank" rel="noreferrer">View Privacy</a><a className="interactive-button interactive-button-secondary" href="/terms" target="_blank" rel="noreferrer">View Terms</a></div>}/>
    {error?<ErrorNotice>{error}</ErrorNotice>:null}
    <div className="interactive-admin-template-layout interactive-admin-legal-layout">{(['privacy','terms'] as const).map((slug)=>{const form=forms[slug];return <section className="interactive-admin-template-editor" key={slug}><div className="interactive-admin-template-editor-head"><div><span className="interactive-eyebrow">{slug}</span><h2>{form.title||slug}</h2></div></div><div className="interactive-admin-form-grid"><div className="interactive-admin-form-span"><Label htmlFor={`legal-${slug}-title`}>Page title</Label><Input id={`legal-${slug}-title`} value={form.title} onChange={(event)=>patch(slug,'title',event.currentTarget.value)}/></div><div className="interactive-admin-form-span"><Label htmlFor={`legal-${slug}-content`}>Content (Markdown)</Label><textarea id={`legal-${slug}-content`} rows={24} value={form.content_markdown||''} onChange={(event)=>patch(slug,'content_markdown',event.currentTarget.value)}/></div></div><div className="interactive-admin-template-actions"><Button disabled={busy===slug||!form.title.trim()} onClick={()=>void save(slug)}>{busy===slug?'Saving…':'Save page'}</Button></div></section>})}</div>
  </>;
}

function AdminChangeLog() {
  const { data, error, refresh } = useAdminData<{ settings: ChangeLogPageSettings; items: ChangeLogEvent[] }>('/api/admin/change-log');
  const today = React.useMemo(() => new Date().toISOString().slice(0, 10), []);
  const blank = React.useCallback((): ChangeLogEvent => ({ id: 0, title: '', content_markdown: '', change_date: today, status: 'published' }), [today]);
  const [settings, setSettings] = React.useState<ChangeLogPageSettings>({ title: 'Change log', intro_markdown: '' });
  const [form, setForm] = React.useState<ChangeLogEvent>(() => blank());
  const [selected, setSelected] = React.useState(0);
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => { if (data?.settings) setSettings(structuredClone(data.settings)); }, [data?.settings?.updated_at]);
  const edit = (item: ChangeLogEvent) => { setSelected(item.id); setForm(structuredClone(item)); };
  const reset = () => { setSelected(0); setForm(blank()); };
  const saveSettings = async () => {
    if (busy) return; setBusy(true);
    try {
      await adminMutate('/api/admin/change-log/settings', 'PATCH', { title: settings.title, introMarkdown: settings.intro_markdown || '' });
      toast.success('Change log page updated.'); await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  const saveEvent = async () => {
    if (busy || !form.title.trim() || !form.change_date) return; setBusy(true);
    const payload = { title: form.title, contentMarkdown: form.content_markdown || '', changeDate: form.change_date.slice(0, 10), status: form.status || 'draft' };
    try {
      if (selected) await adminMutate(`/api/admin/change-log/events/${selected}`, 'PATCH', payload);
      else await adminMutate('/api/admin/change-log/events', 'POST', payload);
      toast.success(selected ? 'Change event updated.' : 'Change event created.'); reset(); await refresh();
    } catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  const remove = async () => {
    if (!selected || busy) return;
    const confirmed = await requestPageConfirm({ title: 'Delete change event?', description: 'This permanently removes the event from the change log.', confirmText: 'Delete', tone: 'destructive' });
    if (!confirmed) return;
    setBusy(true);
    try { await adminMutate(`/api/admin/change-log/events/${selected}`, 'DELETE'); toast.success('Change event deleted.'); reset(); await refresh(); }
    catch (reason) { toast.error(reason instanceof Error ? reason.message : String(reason)); }
    finally { setBusy(false); }
  };
  return <>
    <AdminPageHead title="Change log" copy="Edit the public Change log page and publish dated product changes. Draft events remain private." actions={<a className="interactive-button interactive-button-secondary" href="/change-log" target="_blank" rel="noreferrer">View public page</a>}/>
    {error ? <ErrorNotice>{error}</ErrorNotice> : null}
    <section className="interactive-admin-change-log-settings"><div><span className="interactive-eyebrow">Public page</span><h2>Page content</h2><p>The heading and introduction are stored in the database and can be changed without a deployment.</p></div><div className="interactive-admin-form-grid"><div className="interactive-admin-form-span"><Label htmlFor="change-log-page-title">Page title</Label><Input id="change-log-page-title" value={settings.title || ''} onChange={(event)=>{const title=event.currentTarget.value;setSettings((current)=>({...current,title}))}}/></div><div className="interactive-admin-form-span"><Label htmlFor="change-log-page-intro">Introduction (Markdown)</Label><textarea id="change-log-page-intro" rows={5} value={settings.intro_markdown || ''} onChange={(event)=>{const intro_markdown=event.currentTarget.value;setSettings((current)=>({...current,intro_markdown}))}}/></div></div><Button disabled={busy || !settings.title.trim()} onClick={()=>void saveSettings()}>Save page content</Button></section>
    <div className="interactive-admin-template-layout interactive-admin-change-log-layout">
      <section className="interactive-admin-table-wrap"><table className="interactive-admin-table"><thead><tr><th>Change date</th><th>Change</th><th>Status</th><th/></tr></thead><tbody>{(data?.items || []).map((item)=><tr key={item.id}><td><time dateTime={item.change_date}>{item.change_date.slice(0,10)}</time></td><td><strong>{item.title}</strong></td><td>{item.status}</td><td><button type="button" onClick={()=>edit(item)}>Edit</button></td></tr>)}</tbody></table>{data && data.items.length===0 ? <div className="interactive-empty-state"><p>No change events yet.</p></div> : null}</section>
      <section className="interactive-admin-template-editor"><div className="interactive-admin-template-editor-head"><div><span className="interactive-eyebrow">{selected ? 'Edit change' : 'New change'}</span><h2>{form.title || 'Change event'}</h2></div>{selected ? <Button size="sm" variant="outline" onClick={reset}>New</Button> : null}</div><div className="interactive-admin-form-grid"><div><Label htmlFor="change-log-date">Change date</Label><Input id="change-log-date" type="date" value={form.change_date.slice(0,10)} onChange={(event)=>{const change_date=event.currentTarget.value;setForm((current)=>({...current,change_date}))}}/></div><div><Label htmlFor="change-log-status">Status</Label><select id="change-log-status" value={form.status || 'draft'} onChange={(event)=>{const status=event.currentTarget.value as ChangeLogEvent['status'];setForm((current)=>({...current,status}))}}><option value="published">Published</option><option value="draft">Draft</option></select></div><div className="interactive-admin-form-span"><Label htmlFor="change-log-title">Title</Label><Input id="change-log-title" value={form.title} onChange={(event)=>{const title=event.currentTarget.value;setForm((current)=>({...current,title}))}} placeholder="What changed?"/></div><div className="interactive-admin-form-span"><Label htmlFor="change-log-content">Details (Markdown)</Label><textarea id="change-log-content" rows={12} value={form.content_markdown || ''} onChange={(event)=>{const content_markdown=event.currentTarget.value;setForm((current)=>({...current,content_markdown}))}} placeholder="Describe the improvement, fix or new capability."/></div></div><div className="interactive-admin-template-actions"><Button disabled={busy || !form.title.trim() || !form.change_date} onClick={()=>void saveEvent()}>{selected ? 'Save change' : 'Add change'}</Button>{selected ? <Button variant="destructive" disabled={busy} onClick={()=>void remove()}><Trash2 className="size-4"/> Delete change</Button> : null}</div></section>
    </div>
  </>;
}

function AdminInstitutions() {
  const { data, error, refresh } = useAdminData<{ institutions: AdminInstitutionRecord[] }>('/api/admin/institutions');
  const blank = React.useCallback(():AdminInstitutionRecord => ({id:'',slug:'',name:'',displayName:'',shortName:'',country:'',countryCode:'',region:'',website:'',logo:'',description:'',seoDescription:'',status:'published',aliases:[]}),[]);
  const [form,setForm]=React.useState<AdminInstitutionRecord>(()=>blank());
  const [selected,setSelected]=React.useState('');
  const [busy,setBusy]=React.useState(false);
  const logoRef=React.useRef<HTMLInputElement>(null);
  const edit=(item:AdminInstitutionRecord)=>{setSelected(item.id);setForm(structuredClone(item))};
  const change=<K extends keyof AdminInstitutionRecord>(key:K,value:AdminInstitutionRecord[K])=>setForm((current)=>({...current,[key]:value}));
  const save=async()=>{if(busy)return;setBusy(true);try{if(selected)await adminMutate(`/api/admin/institutions/${encodeURIComponent(selected)}`,'PATCH',form);else await adminMutate('/api/admin/institutions','POST',{...form,id:form.id||form.slug});toast.success('Institution saved.');setSelected('');setForm(blank());await refresh()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setBusy(false)}};
  const uploadLogo=async(file?:File)=>{if(!file||!selected||busy)return;setBusy(true);try{if(!['image/png','image/jpeg','image/webp'].includes(file.type)&&!/[.](png|jpe?g|webp)$/iu.test(file.name)){toast.error('Institution logos must be PNG, JPEG or WebP.');return}const result=await adminUploadInstitutionLogo(selected,file);setForm(structuredClone(result.institution));toast.success('Institution logo uploaded.');await refresh()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setBusy(false);if(logoRef.current)logoRef.current.value=''}};
  const clearLogo=async()=>{if(!selected||busy)return;setBusy(true);try{const result=await adminClearInstitutionLogo(selected);setForm(structuredClone(result.institution));toast.success('Institution logo removed.');await refresh()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}finally{setBusy(false)}};
  const merge=async()=>{if(!selected)return;const target=await requestPagePrompt({title:'Merge institution',description:'Move all templates to an existing institution ID, then remove this duplicate. If the target has no logo, Learnitive also preserves this institution logo.',label:'Target institution ID',confirmText:'Merge'});if(!target)return;try{await adminMutate('/api/admin/institutions/merge','POST',{fromId:selected,intoId:target.trim()});toast.success('Institution merged.');setSelected('');setForm(blank());await refresh()}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}};
  return <>
    <AdminPageHead title="Academic Institutions" copy="Maintain a reusable university/institution taxonomy so templates do not fragment across naming variants such as UNSW, UNSW Sydney and University of New South Wales."/>
    {error?<ErrorNotice>{error}</ErrorNotice>:null}
    <div className="interactive-admin-template-layout interactive-admin-institution-layout">
      <section className="interactive-admin-table-wrap"><table className="interactive-admin-table"><thead><tr><th>Institution</th><th>Country</th><th>Region</th><th>Status</th><th/></tr></thead><tbody>{(data?.institutions||[]).map((item)=><tr key={item.id}><td><div className="interactive-admin-institution-list-name">{item.logo?<img src={`/api/templates/institutions/${encodeURIComponent(item.id)}/logo`} alt=""/>:null}<div><strong>{item.displayName}</strong><br/><code>{item.id}</code></div></div></td><td>{item.country||'—'}</td><td>{item.region||'—'}</td><td>{item.status}</td><td><button type="button" onClick={()=>edit(item)}>Edit</button></td></tr>)}</tbody></table></section>
      <section className="interactive-admin-template-editor"><div className="interactive-admin-template-editor-head"><div><span className="interactive-eyebrow">{selected?'Edit institution':'New institution'}</span><h2>{form.displayName||'Academic institution'}</h2></div>{selected?<Button size="sm" variant="outline" onClick={()=>{setSelected('');setForm(blank())}}>New</Button>:null}</div><div className="interactive-admin-form-grid"><div><Label>ID / slug</Label><Input disabled={Boolean(selected)} value={form.id} onChange={(e)=>{const id=e.currentTarget.value.toLowerCase();setForm((current)=>({...current,id,slug:id}))}} placeholder="unsw"/></div><div><Label>Canonical name</Label><Input value={form.name} onChange={(e)=>change('name',e.currentTarget.value)} placeholder="University of New South Wales"/></div><div><Label>Display name</Label><Input value={form.displayName} onChange={(e)=>change('displayName',e.currentTarget.value)} placeholder="UNSW Sydney"/></div><div><Label>Short name</Label><Input value={form.shortName||''} onChange={(e)=>change('shortName',e.currentTarget.value)} placeholder="UNSW"/></div><div><Label>Country</Label><Input value={form.country||''} onChange={(e)=>change('country',e.currentTarget.value)}/></div><div><Label>Country code</Label><Input value={form.countryCode||''} onChange={(e)=>change('countryCode',e.currentTarget.value.toUpperCase())}/></div><div><Label>State / region</Label><Input value={form.region||''} onChange={(e)=>change('region',e.currentTarget.value)}/></div><div><Label>Status</Label><select value={form.status} onChange={(e)=>change('status',e.currentTarget.value as AdminInstitutionRecord['status'])}><option value="published">Published</option><option value="draft">Draft</option><option value="archived">Archived</option></select></div><div className="interactive-admin-form-span"><Label>Official website</Label><Input type="url" value={form.website||''} onChange={(e)=>change('website',e.currentTarget.value)}/></div><div className="interactive-admin-form-span interactive-admin-institution-logo"><div className="interactive-admin-institution-logo-head"><div><Label>Institution logo</Label><p>Optional PNG, JPEG or WebP up to 5 MB. Logos appear in the public university directory and template discovery cards.</p></div>{selected?<div className="interactive-admin-template-preview-actions"><input ref={logoRef} type="file" accept="image/png,image/jpeg,image/webp" className="sr-only" onChange={(event)=>void uploadLogo(event.currentTarget.files?.[0])}/><Button type="button" size="sm" variant="outline" disabled={busy} onClick={()=>logoRef.current?.click()}>{form.logo?'Replace logo':'Upload logo'}</Button>{form.logo?<Button type="button" size="sm" variant="ghost" disabled={busy} onClick={()=>void clearLogo()}>Remove logo</Button>:null}</div>:<span className="interactive-muted">Save the institution before uploading a logo.</span>}</div>{form.logo&&selected?<div className="interactive-admin-institution-logo-preview"><img src={`/api/templates/institutions/${encodeURIComponent(selected)}/logo`} alt={`${form.displayName||form.name} logo`}/><code>{form.logo}</code></div>:null}</div><div className="interactive-admin-form-span"><Label>Aliases</Label><Input value={adminCsv(form.aliases)} onChange={(e)=>change('aliases',parseAdminList(e.currentTarget.value))} placeholder="UNSW, University of NSW"/></div><div className="interactive-admin-form-span"><Label>Description</Label><textarea rows={4} value={form.description||''} onChange={(e)=>change('description',e.currentTarget.value)}/></div><div className="interactive-admin-form-span"><Label>SEO description</Label><textarea rows={3} value={form.seoDescription||''} onChange={(e)=>change('seoDescription',e.currentTarget.value)}/></div></div><div className="interactive-admin-template-actions"><Button disabled={busy||!form.id.trim()||!form.name.trim()||!form.displayName.trim()} onClick={()=>void save()}>Save institution</Button>{selected?<Button variant="outline" onClick={()=>void merge()}>Merge duplicate</Button>:null}</div></section>
    </div>
  </>;
}


function AdminSupport(){const [page,setPage]=React.useState(1);const [search,setSearch]=React.useState('');const [status,setStatus]=React.useState('');const [selected,setSelected]=React.useState<Record<string,unknown>>();const path=`/api/admin/support?page=${page}&pageSize=25&search=${encodeURIComponent(search)}&status=${encodeURIComponent(status)}`;const {data,error,refresh}=useAdminData<{items:Array<Record<string,unknown>>;page:number;pageSize:number;total:number}>(path);const update=async(id:number,nextStatus:string)=>{await adminMutate(`/api/admin/support/${id}`,'PATCH',{status:nextStatus});if(Number(selected?.id)===id)setSelected({...selected,status:nextStatus});await refresh()};const view=async(id:number)=>{try{const result=await adminGet<{item:Record<string,unknown>}>(`/api/admin/support/${id}`);setSelected(result.item)}catch(reason){toast.error(reason instanceof Error?reason.message:String(reason))}};const changeSearch=(value:string)=>{setSearch(value);setPage(1)};return <><AdminPageHead title="Support" copy="Search contact requests, inspect ticket details and update status without exposing message bodies in list responses."/><AdminListControls search={search} onSearch={changeSearch}><select value={status} onChange={(event)=>{setStatus(event.currentTarget.value);setPage(1)}} aria-label="Filter tickets by status"><option value="">All statuses</option><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select></AdminListControls>{error?<ErrorNotice>{error}</ErrorNotice>:null}<AdminTable rows={data?.items||[]} actions={(row)=><><button type="button" onClick={()=>void view(Number(row.id))}>View</button><select value={String(row.status||'open')} onChange={(event)=>void update(Number(row.id),event.currentTarget.value)}><option value="open">Open</option><option value="in_progress">In progress</option><option value="resolved">Resolved</option><option value="closed">Closed</option></select></>}/>{data?<AdminPager page={data.page} pageSize={data.pageSize} total={data.total} onPage={setPage}/>:null}{selected?<article className="interactive-support-detail"><div><span className="interactive-eyebrow">Ticket #{String(selected.id)}</span><h2>{String(selected.subject||'Support request')}</h2><p>{String(selected.name||'')} · {String(selected.email||'')}</p></div><button type="button" className="interactive-icon-button" aria-label="Close ticket detail" onClick={()=>setSelected(undefined)}>×</button><div className="interactive-support-message">{String(selected.message||'')}</div><dl><div><dt>Status</dt><dd>{String(selected.status||'')}</dd></div><div><dt>Account</dt><dd>{String(selected.account_name||'Not linked')}</dd></div><div><dt>Created</dt><dd>{String(selected.created_at||'')}</dd></div></dl></article>:null}</>}
function AdminTable({rows,actions}:{rows:Array<Record<string,unknown>>;actions?:(row:Record<string,unknown>)=>React.ReactNode}){if(!rows.length)return <div className="interactive-empty-state"><p>No records.</p></div>;const keys=Object.keys(rows[0]||{}).slice(0,8);return <div className="interactive-admin-table-wrap"><table className="interactive-admin-table"><thead><tr>{keys.map((key)=><th key={key}>{key}</th>)}{actions?<th>Actions</th>:null}</tr></thead><tbody>{rows.map((row,index)=><tr key={String(row.id??index)}>{keys.map((key)=><td key={key}>{String(row[key]??'')}</td>)}{actions?<td className="interactive-admin-actions">{actions(row)}</td>:null}</tr>)}</tbody></table></div>}
