/**
 * LinkMLViewerPage.tsx - LinkML Schema Viewer Page
 * 
 * Displays LinkML schema files with:
 * - Sidebar listing schemas by category (main, classes, enums, slots)
 * - Visual display of selected schema showing classes, slots, and enums
 * - Raw YAML view toggle
 * - Schema metadata and documentation
 * - URL parameter support for deep linking to specific classes (?class=ClassName)
 */

import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import debounce from 'lodash/debounce';
import { useSearchParams } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import {
  loadSchema,
  loadSchemaRaw,
  type LinkMLSchema,
  type LinkMLClass,
  type LinkMLSlot,
  type LinkMLEnum,
  type SchemaFile,
  extractClasses,
  extractSlots,
  extractEnums,
} from '../lib/linkml/schema-loader';
import { linkmlSchemaService, type ClassExportInfo, type ClassImportInfo, type ClassDependencyCounts, type SlotDefinition } from '../lib/linkml/linkml-schema-service';
import { useLanguage } from '../contexts/LanguageContext';
import { useSchemaLoadingProgress } from '../hooks/useSchemaLoadingProgress';
import { CustodianTypeBadge } from '../components/uml/CustodianTypeIndicator';
import { CustodianTypeIndicator3D } from '../components/uml/CustodianTypeIndicator3D';
import { CustodianTypeLegendBar } from '../components/uml/CustodianTypeLegend';
import { UMLVisualization, type UMLDiagram, type UMLNode, type UMLLink } from '../components/uml/UMLVisualization';
import { CUSTODIAN_TYPE_CODES, type CustodianTypeCode } from '../lib/custodian-types';
import { LoadingScreen } from '../components/LoadingScreen';
import {
  getCustodianTypesForClass,
  getCustodianTypesForSlot,
  getCustodianTypesForEnum,
  getCustodianTypesForClassAsync,
  getCustodianTypesForSlotAsync,
  getCustodianTypesForEnumAsync,
  isUniversalElement,
} from '../lib/schema-custodian-mapping';
import './LinkMLViewerPage.css';

/**
 * Converts snake_case or kebab-case identifiers to human-readable Title Case.
 * 
 * Examples:
 * - "custodian_appellation_class" → "Custodian Appellation Class"
 * - "feature_place_class" → "Feature Place Class"
 * - "heritage-custodian-observation" → "Heritage Custodian Observation"
 */
const formatDisplayName = (name: string): string => {
  if (!name) return name;
  
  return name
    // Replace underscores and hyphens with spaces
    .replace(/[_-]/g, ' ')
    // Capitalize first letter of each word
    .replace(/\b\w/g, char => char.toUpperCase());
};

/**
 * Admonition transformer for markdown content.
 * Converts patterns like "CRITICAL: message" into styled admonition elements.
 * 
 * Supported patterns:
 * - CRITICAL: Urgent, must-know information (red)
 * - WARNING: Potential issues or gotchas (orange)
 * - IMPORTANT: Significant but not urgent (amber)
 * - NOTE: Informational, helpful context (blue)
 * 
 * The pattern captures everything from the keyword until:
 * - End of line (for single-line admonitions)
 * - A blank line (paragraph break)
 */
const transformAdmonitions = (text: string): string => {
  if (!text) return text;
  
  // Pattern matches: KEYWORD: rest of the text until end of paragraph
  // [^\n]* captures everything on the current line
  // (?:\n(?!\n)[^\n]*)* captures continuation lines (lines that don't start a new paragraph)
  const admonitionPattern = /\b(CRITICAL|WARNING|IMPORTANT|NOTE):\s*([^\n]*(?:\n(?!\n)[^\n]*)*)/g;
  
  return text.replace(admonitionPattern, (_match, keyword, content) => {
    const type = keyword.toLowerCase();
    // Return HTML that ReactMarkdown will pass through
    return `<span class="linkml-admonition linkml-admonition--${type}"><span class="linkml-admonition__label">${keyword}:</span><span class="linkml-admonition__content">${content.trim()}</span></span>`;
  });
};

/**
 * CURIE (Compact URI) highlighter for markdown content.
 * Highlights patterns like "schema:name", "crm:E41_Appellation", "skos:altLabel".
 * 
 * CURIE format: prefix:localName
 * - prefix: lowercase letters (e.g., schema, crm, skos, foaf, dcterms)
 * - localName: letters, numbers, underscores, hyphens (e.g., name, E41_Appellation)
 * 
 * Also handles compound CURIEs like "schema:dateCreated/dateModified"
 */
const highlightCuries = (text: string): string => {
  if (!text) return text;
  
  // Common ontology prefixes used in heritage/linked data
  const knownPrefixes = [
    'schema', 'crm', 'skos', 'foaf', 'dcterms', 'dct', 'dc', 'rdfs', 'rdf', 'owl',
    'prov', 'org', 'locn', 'cpov', 'tooi', 'rico', 'bf', 'wikidata', 'wd', 'wdt',
    'xsd', 'linkml', 'hc', 'pico', 'cv'
  ].join('|');
  
  // Pattern matches: prefix:localName (with optional /additional parts)
  // Negative lookbehind (?<![:/]) prevents matching URLs like http://...
  // Negative lookahead (?![:/]) prevents partial URL matches
  const curiePattern = new RegExp(
    `(?<![:/])\\b((?:${knownPrefixes}):(?:[A-Za-z][A-Za-z0-9_-]*(?:/[A-Za-z][A-Za-z0-9_-]*)*))(?![:/])`,
    'g'
  );
  
  return text.replace(curiePattern, '<code class="linkml-curie">$1</code>');
};

/**
 * ASCII Tree Diagram transformer.
 * Converts ASCII tree diagrams (using ├── └── │ characters) into styled HTML trees.
 * 
 * Detects patterns like:
 * ```
 * Archives nationales (national)
 *   └── Archives régionales (regional)
 *         └── Archives départementales (THIS TYPE)
 *               └── Archives communales (municipal)
 * ```
 * 
 * Or more complex trees:
 * ```
 * Custodian (hub)
 *   │
 *   └── CustodianCollection (aspect)
 *         ├── CollectionType (classification)
 *         ├── AccessPolicy (access restrictions)
 *         └── sub_collections → Collection[]
 * ```
 */
const transformTreeDiagrams = (text: string): string => {
  if (!text) return text;
  
  const codeBlockPattern = /```\n?([\s\S]*?)\n?```/g;
  
  return text.replace(codeBlockPattern, (match, codeContent: string) => {
    // Check if this code block contains tree characters but NOT box diagrams
    const hasTreeChars = /[├└│]──/.test(codeContent) || /\s+└──/.test(codeContent);
    const hasBoxChars = /[┌┐]/.test(codeContent); // Only top box corners indicate box diagram
    
    if (!hasTreeChars || hasBoxChars) {
      // Not a tree diagram (or is a box diagram), skip
      return match;
    }
    
    const lines = codeContent.split('\n').filter(l => l.trim());
    if (lines.length === 0) return match;
    
    // First pass: detect unique indentation levels to build proper hierarchy
    const indentLevels = new Set<number>();
    for (const line of lines) {
      const leadingSpaces = line.match(/^(\s*)/)?.[1]?.length || 0;
      indentLevels.add(leadingSpaces);
    }
    // Sort indent levels to map them to 0, 1, 2, 3...
    const sortedIndents = Array.from(indentLevels).sort((a, b) => a - b);
    const indentMap = new Map<number, number>();
    sortedIndents.forEach((indent, index) => indentMap.set(indent, index));
    
    // Build tree structure
    let html = '<div class="linkml-tree-diagram">';
    
    for (const line of lines) {
      // Get actual indent level from map
      const leadingSpaces = line.match(/^(\s*)/)?.[1]?.length || 0;
      const indentLevel = indentMap.get(leadingSpaces) || 0;
      
      // Check for tree branch characters
      const hasBranch = /├──|└──/.test(line);
      const isLastBranch = /└──/.test(line);
      const hasVerticalLine = /│/.test(line) && !/[├└]/.test(line);
      
      // Check if this line is highlighted (THIS CLASS, THIS TYPE, etc.)
      const isHighlighted = /\(THIS\s*(CLASS|TYPE|LEVEL)?\)/.test(line);
      
      // Extract the content after tree characters
      let content = line
        .replace(/^[\s│├└─]+/, '') // Remove leading spaces and tree chars
        .replace(/→/g, '<span class="linkml-tree-arrow">→</span>') // Style arrows
        .trim();
      
      // Clean up and add highlighting
      if (isHighlighted) {
        content = content.replace(/\(THIS\s*(CLASS|TYPE|LEVEL)?\)/g, '');
        content = `<span class="linkml-tree-highlight">${content.trim()}</span>`;
      }
      
      // Skip empty vertical connector lines
      if (hasVerticalLine && !content) {
        html += `<div class="linkml-tree-connector" style="margin-left: ${indentLevel * 1.5}rem;">│</div>`;
        continue;
      }
      
      // Determine the branch character to display
      let branchChar = '';
      if (isLastBranch) {
        branchChar = '<span class="linkml-tree-branch">└──</span>';
      } else if (hasBranch) {
        branchChar = '<span class="linkml-tree-branch">├──</span>';
      }
      
      if (content) {
        const itemClass = isHighlighted ? 'linkml-tree-item linkml-tree-item--highlighted' : 'linkml-tree-item';
        html += `<div class="${itemClass}" style="margin-left: ${indentLevel * 1.5}rem;">${branchChar}${content}</div>`;
      }
    }
    
    html += '</div>';
    return html;
  });
};

/**
 * Arrow-based Flow Diagram transformer.
 * Converts simple arrow-based flow diagrams (using ↓ → ← characters) into styled HTML.
 * 
 * Detects patterns like:
 * ```
 * Current Archive (active use)
 *   ↓
 * DEPOSIT ARCHIVE (semi-current) ← THIS TYPE
 *   ↓
 * Historical Archive (permanent preservation)
 *   or
 * Destruction (per retention schedule)
 * ```
 * 
 * Or horizontal flows:
 * ```
 * DepositArchive (custodian type)
 *   │
 *   └── operates_storage → Storage (facility instance)
 *                            │
 *                            └── has_storage_type → StorageType
 *                                  └── DEPOSIT_STORAGE
 * ```
 */
const transformFlowDiagrams = (text: string): string => {
  if (!text) return text;
  
  const codeBlockPattern = /```\n?([\s\S]*?)\n?```/g;
  
  return text.replace(codeBlockPattern, (match, codeContent: string) => {
    // Skip if it has tree branch characters (├── └──) - handled by tree transformer
    const hasTreeChars = /[├└]──/.test(codeContent);
    // Skip if it has box characters (┌─┐) - handled by lifecycle transformer
    const hasBoxChars = /[┌┐]/.test(codeContent);
    
    if (hasTreeChars || hasBoxChars) {
      return match; // Let other transformers handle these
    }
    
    // Check if this is an arrow-based flow diagram
    // Must have vertical arrows (↓) or be a simple vertical flow with indented items
    const hasVerticalArrows = /↓/.test(codeContent);
    const hasHorizontalArrows = /[←→]/.test(codeContent);
    const hasVerticalPipe = /│/.test(codeContent);
    
    // Must have at least arrows to be considered a flow diagram
    if (!hasVerticalArrows && !hasHorizontalArrows && !hasVerticalPipe) {
      return match;
    }
    
    const lines = codeContent.split('\n');
    const elements: Array<{
      type: 'node' | 'arrow' | 'branch' | 'connector';
      content: string;
      annotation?: string;
      isHighlighted?: boolean;
      indentLevel?: number;
    }> = [];
    
    // Detect indent levels
    const indentLevels = new Set<number>();
    for (const line of lines) {
      if (line.trim()) {
        const leadingSpaces = line.match(/^(\s*)/)?.[1]?.length || 0;
        indentLevels.add(leadingSpaces);
      }
    }
    const sortedIndents = Array.from(indentLevels).sort((a, b) => a - b);
    const indentMap = new Map<number, number>();
    sortedIndents.forEach((indent, index) => indentMap.set(indent, index));
    
    for (const line of lines) {
      const trimmedLine = line.trim();
      if (!trimmedLine) continue;
      
      const leadingSpaces = line.match(/^(\s*)/)?.[1]?.length || 0;
      const indentLevel = indentMap.get(leadingSpaces) || 0;
      
      // Check for pure arrow lines
      if (/^[↓⬇]+$/.test(trimmedLine)) {
        elements.push({ type: 'arrow', content: '↓', indentLevel });
        continue;
      }
      
      // Check for pure vertical connector lines
      if (/^│+$/.test(trimmedLine)) {
        elements.push({ type: 'connector', content: '│', indentLevel });
        continue;
      }
      
      // Check for "or" / "and" branching keywords
      if (/^(or|and|OR|AND)$/i.test(trimmedLine)) {
        elements.push({ type: 'branch', content: trimmedLine.toLowerCase(), indentLevel });
        continue;
      }
      
      // This is a node - check for highlighting markers and annotations
      let content = trimmedLine;
      let annotation = '';
      let isHighlighted = false;
      
      // Check for THIS TYPE/CLASS markers
      if (/←\s*(THIS\s*(TYPE|CLASS)?|DEZE\s*(TYPE|KLASSE)?)/i.test(content)) {
        isHighlighted = true;
        // Extract the marker as annotation
        const markerMatch = content.match(/←\s*(THIS\s*(TYPE|CLASS)?|DEZE\s*(TYPE|KLASSE)?)/i);
        if (markerMatch) {
          annotation = markerMatch[1];
          content = content.replace(/\s*←\s*(THIS\s*(TYPE|CLASS)?|DEZE\s*(TYPE|KLASSE)?)/gi, '');
        }
      }
      
      // Check for parenthetical annotations like "(active use)"
      const parenMatch = content.match(/\(([^)]+)\)\s*$/);
      if (parenMatch && !isHighlighted) {
        annotation = parenMatch[1];
        content = content.replace(/\s*\([^)]+\)\s*$/, '');
      }
      
      // Clean up any remaining tree characters that might have slipped through
      content = content.replace(/^[│\s]+/, '').trim();
      
      if (content) {
        elements.push({ 
          type: 'node', 
          content, 
          annotation: annotation || undefined,
          isHighlighted, 
          indentLevel 
        });
      }
    }
    
    // If we didn't parse meaningful elements, return original
    if (elements.length < 2) {
      return match;
    }
    
    // Build HTML output
    let html = '<div class="linkml-flow-diagram">';
    
    for (const element of elements) {
      const indentStyle = element.indentLevel ? ` style="margin-left: ${element.indentLevel * 1.5}rem;"` : '';
      
      if (element.type === 'node') {
        const highlightClass = element.isHighlighted ? ' linkml-flow-node--highlighted' : '';
        html += `<div class="linkml-flow-node${highlightClass}"${indentStyle}>`;
        html += `<span class="linkml-flow-node__content">${element.content}</span>`;
        if (element.annotation) {
          html += `<span class="linkml-flow-node__annotation">${element.annotation}</span>`;
        }
        html += '</div>';
      } else if (element.type === 'arrow') {
        html += `<div class="linkml-flow-arrow"${indentStyle}>↓</div>`;
      } else if (element.type === 'connector') {
        html += `<div class="linkml-flow-connector"${indentStyle}>│</div>`;
      } else if (element.type === 'branch') {
        html += `<div class="linkml-flow-branch"${indentStyle}>${element.content}</div>`;
      }
    }
    
    html += '</div>';
    return html;
  });
};

/**
 * ASCII Lifecycle Diagram transformer.
 * Converts ASCII box diagrams (using ┌─┐│└─┘ characters) into styled HTML cards.
 * 
 * Detects patterns like:
 * ```
 * ┌─────────────────────────────────────┐
 * │  CustodianAdministration            │
 * │  ═════════════════════════          │
 * │  ACTIVE records in daily use        │
 * └─────────────────────────────────────┘
 *                   ↓
 * ┌─────────────────────────────────────┐
 * │  CustodianArchive                   │
 * └─────────────────────────────────────┘
 * ```
 * 
 * And converts them to styled HTML cards with flow arrows.
 */
const transformLifecycleDiagrams = (text: string): string => {
  if (!text) return text;
  
  // First transform tree diagrams, then flow diagrams
  text = transformTreeDiagrams(text);
  text = transformFlowDiagrams(text);
  
  // Pattern to match markdown code blocks containing ASCII box diagrams
  // Looks for ``` followed by content containing box-drawing characters
  const codeBlockPattern = /```\n?([\s\S]*?)\n?```/g;
  
  return text.replace(codeBlockPattern, (match, codeContent: string) => {
    // Check if this code block contains ASCII box diagrams (with top corners)
    const hasBoxChars = /[┌┐└┘│─═]/.test(codeContent) && /[┌┐]/.test(codeContent);
    if (!hasBoxChars) {
      // Not a box diagram, return original code block
      return match;
    }
    
    // Parse the diagram into boxes and connectors
    const lines = codeContent.split('\n');
    const elements: Array<{ type: 'box' | 'arrow' | 'text'; content: string; title?: string; isHighlighted?: boolean }> = [];
    let currentBox: string[] = [];
    let inBox = false;
    
    for (const line of lines) {
      const trimmedLine = line.trim();
      
      // Check for box start
      if (trimmedLine.startsWith('┌') && trimmedLine.endsWith('┐')) {
        inBox = true;
        currentBox = [];
        continue;
      }
      
      // Check for box end
      if (trimmedLine.startsWith('└') && trimmedLine.endsWith('┘')) {
        if (currentBox.length > 0) {
          // Extract title (first non-empty line or line with ═)
          let title = '';
          let content: string[] = [];
          let isHighlighted = false;
          
          for (let i = 0; i < currentBox.length; i++) {
            const boxLine = currentBox[i];
            // Check if this is a title line (followed by ═ underline or contains "(THIS CLASS)")
            if (i === 0 || boxLine.includes('═') || boxLine.includes('THIS CLASS')) {
              if (boxLine.includes('═')) {
                // This is an underline, title was previous line
                continue;
              }
              if (boxLine.includes('THIS CLASS') || boxLine.includes('(THIS')) {
                isHighlighted = true;
                title = boxLine.replace(/\(THIS CLASS\)/g, '').replace(/\(THIS\)/g, '').trim();
              } else if (!title && boxLine.trim()) {
                title = boxLine.trim();
              } else {
                content.push(boxLine);
              }
            } else {
              content.push(boxLine);
            }
          }
          
          // Clean up title - remove leading/trailing special characters
          title = title.replace(/^[═\s]+|[═\s]+$/g, '').trim();
          
          elements.push({
            type: 'box',
            title: title || 'Untitled',
            content: content.filter(l => l.trim() && !l.includes('═')).join('\n'),
            isHighlighted
          });
        }
        inBox = false;
        continue;
      }
      
      // Inside a box - collect content
      if (inBox) {
        // Remove box side characters and clean up
        const cleanLine = trimmedLine
          .replace(/^│\s*/, '')
          .replace(/\s*│$/, '')
          .trim();
        if (cleanLine) {
          currentBox.push(cleanLine);
        }
        continue;
      }
      
      // Check for arrow/connector lines
      if (trimmedLine.includes('↓') || trimmedLine.includes('→') || trimmedLine.includes('⬇')) {
        elements.push({ type: 'arrow', content: '↓' });
        continue;
      }
      
      // Check for text between boxes (descriptions of transitions)
      if (trimmedLine.startsWith('(') && (trimmedLine.endsWith(')') || trimmedLine.includes(')'))) {
        // This is explanatory text in parentheses
        const textContent = trimmedLine.replace(/^\(|\)$/g, '').trim();
        elements.push({ type: 'text', content: textContent });
        continue;
      }
      
      // Other non-empty lines between boxes (transition descriptions)
      if (trimmedLine && !trimmedLine.match(/^[─┌┐└┘│═]+$/)) {
        // Continuation of previous text or standalone text
        const lastElement = elements[elements.length - 1];
        if (lastElement && lastElement.type === 'text') {
          lastElement.content += ' ' + trimmedLine.replace(/^\(|\)$/g, '');
        } else if (trimmedLine.length > 2) {
          elements.push({ type: 'text', content: trimmedLine.replace(/^\(|\)$/g, '') });
        }
      }
    }
    
    // If no elements were parsed, return original
    if (elements.length === 0) {
      return match;
    }
    
    // Build HTML output
    let html = '<div class="linkml-lifecycle-diagram">';
    
    for (const element of elements) {
      if (element.type === 'box') {
        const highlightClass = element.isHighlighted ? ' linkml-lifecycle-box--highlighted' : '';
        html += `<div class="linkml-lifecycle-box${highlightClass}">`;
        html += `<div class="linkml-lifecycle-box__title">${element.title}</div>`;
        if (element.content) {
          // Convert content lines to list items or paragraphs
          const contentLines = element.content.split('\n').filter(l => l.trim());
          if (contentLines.length > 0) {
            html += '<div class="linkml-lifecycle-box__content">';
            for (const line of contentLines) {
              // Check if it's a list item (starts with -)
              if (line.trim().startsWith('-')) {
                html += `<div class="linkml-lifecycle-box__item">${line.trim().substring(1).trim()}</div>`;
              } else if (line.trim().startsWith('✅') || line.trim().startsWith('❌')) {
                html += `<div class="linkml-lifecycle-box__item">${line.trim()}</div>`;
              } else {
                html += `<div class="linkml-lifecycle-box__line">${line}</div>`;
              }
            }
            html += '</div>';
          }
        }
        html += '</div>';
      } else if (element.type === 'arrow') {
        html += '<div class="linkml-lifecycle-arrow">↓</div>';
      } else if (element.type === 'text') {
        html += `<div class="linkml-lifecycle-text">${element.content}</div>`;
      }
    }
    
    html += '</div>';
    return html;
  });
};

/**
 * Combined content transformer that applies all text transformations.
 * Order matters: 
 * 1. Lifecycle diagrams first (process code blocks before other transformations)
 * 2. CURIEs (so they don't get broken by admonition spans)
 * 3. Admonitions last
 */
const transformContent = (text: string): string => {
  if (!text) return text;
  return transformAdmonitions(highlightCuries(transformLifecycleDiagrams(text)));
};

// Debug logging helper
const debugLog = (...args: unknown[]) => {
  console.log('[LinkMLViewerPage]', ...args);
};

/**
 * Build a filtered UML diagram centered on a specific class.
 * Includes both imports (dependencies) and exports (reverse dependencies) based on flags.
 * Uses BFS to include related classes up to specified depth.
 * 
 * @param centerClassName - The class to center the diagram on
 * @param allExportInfo - Map of class name to ClassExportInfo for all loaded classes
 * @param allImportInfo - Map of class name to ClassImportInfo for all loaded classes
 * @param depth - How many levels of relationships to include (default: 1)
 * @param parentClasses - Optional map of class to parent class from semantic info
 * @param showImports - Whether to include import relationships (dependencies)
 * @param showExports - Whether to include export relationships (reverse dependencies)
 * @param slotSemanticLabels - Optional map of slot name to semantic predicate label (e.g., "has_collection" -> "schema:collection")
 * @returns UMLDiagram with filtered nodes and links
 */
const buildFilteredUMLDiagram = (
  centerClassName: string,
  allExportInfo: Record<string, ClassExportInfo>,
  allImportInfo: Record<string, ClassImportInfo>,
  depth: number = 1,
  parentClasses: Record<string, string> = {},
  showImports: boolean = true,
  showExports: boolean = true,
  slotSemanticLabels: Map<string, string> = new Map()
): UMLDiagram => {
  const nodes: UMLNode[] = [];
  const links: UMLLink[] = [];
  const addedNodes = new Set<string>();
  const addedLinks = new Set<string>(); // Track links to avoid duplicates
  const processedAtDepth = new Map<string, number>(); // Track at what depth each class was processed
  
  // Helper to get the semantic label for a slot (falls back to slot name if not found)
  const getSemanticLabel = (slotName: string): string => {
    return slotSemanticLabels.get(slotName) || slotName;
  };
  
  // Helper to add a node if not already added
  const addNode = (name: string, _distanceFromCenter: number, type: 'class' | 'enum' = 'class') => {
    if (!addedNodes.has(name)) {
      addedNodes.add(name);
      nodes.push({
        id: name,
        name: name,
        type: type,
        attributes: [],
        methods: [],
      });
    }
  };
  
  // Helper to add a link if not already added (checking both directions)
  // For slot-based edges (aggregation/composition), uses semantic predicate as label
  const addLink = (source: string, target: string, type: UMLLink['type'], label: string, isSlotEdge: boolean = false) => {
    const linkKey1 = `${source}->${target}:${type}`;
    const linkKey2 = `${target}->${source}:${type}`;
    // For slot edges, use semantic predicate label; for others (inheritance, mixin), use original label
    const displayLabel = isSlotEdge ? getSemanticLabel(label) : label;
    // For inheritance, direction matters; for others, check both
    if (type === 'inheritance') {
      if (!addedLinks.has(linkKey1)) {
        addedLinks.add(linkKey1);
        links.push({ source, target, type, label: displayLabel });
      }
    } else {
      if (!addedLinks.has(linkKey1) && !addedLinks.has(linkKey2)) {
        addedLinks.add(linkKey1);
        links.push({ source, target, type, label: displayLabel });
      }
    }
  };
  
  // Get all related classes based on showImports/showExports flags
  const getRelatedClasses = (className: string): string[] => {
    const related: string[] = [];
    
    // IMPORTS: What this class depends on (parent, mixins, slot ranges)
    if (showImports) {
      const importInfo = allImportInfo[className];
      if (importInfo) {
        // Parent class (is_a)
        if (importInfo.parentClass) {
          related.push(importInfo.parentClass);
        }
        // Mixins this class uses
        related.push(...importInfo.mixins);
        // Classes/enums used as slot ranges
        related.push(...importInfo.slotRanges.map(r => r.rangeType));
        // Slot usage range overrides
        related.push(...importInfo.slotUsageRanges.map(r => r.rangeType));
        // Annotation-based relationships (dual-class pattern)
        if (importInfo.linkedCollectionType) {
          related.push(importInfo.linkedCollectionType);
        }
        if (importInfo.linkedCustodianType) {
          related.push(importInfo.linkedCustodianType);
        }
      }
      // Also check parentClasses map for inheritance
      if (parentClasses[className]) {
        related.push(parentClasses[className]);
      }
    }
    
    // EXPORTS: What references this class (subclasses, mixin users, slot ranges)
    if (showExports) {
      const exportInfo = allExportInfo[className];
      if (exportInfo) {
        // Subclasses
        related.push(...exportInfo.subclasses);
        // Mixin users
        related.push(...exportInfo.mixinUsers);
        // Classes using slots with this range
        related.push(...exportInfo.classesUsingSlotWithThisRange.map(c => c.className));
        // Classes referencing in slot_usage
        related.push(...exportInfo.classesReferencingInSlotUsage);
        // Annotation-based relationships (dual-class pattern) - collection types that reference this custodian
        related.push(...exportInfo.linkedCollectionTypes);
      }
    }
    
    return [...new Set(related)]; // Dedupe
  };
  
  // Add relationships from a class based on showImports/showExports flags
  const addRelationshipsFromClass = (className: string) => {
    // IMPORTS: Show what this class depends on
    if (showImports) {
      const importInfo = allImportInfo[className];
      if (importInfo) {
        // Parent class (is_a inheritance) - arrow points from child to parent
        if (importInfo.parentClass && addedNodes.has(importInfo.parentClass)) {
          addLink(className, importInfo.parentClass, 'inheritance', 'is_a', false);
        }
        // Mixins - arrow points from class to mixin
        for (const mixin of importInfo.mixins) {
          if (addedNodes.has(mixin)) {
            addLink(className, mixin, 'association', 'mixin', false);
          }
        }
        // Slot ranges - arrow points from class to range type (SLOT EDGE - use semantic predicate)
        for (const { slotName, rangeType } of importInfo.slotRanges) {
          if (addedNodes.has(rangeType)) {
            addLink(className, rangeType, 'aggregation', slotName, true);
          }
        }
        // Slot usage ranges (SLOT EDGE - use semantic predicate)
        for (const { slotName, rangeType } of importInfo.slotUsageRanges) {
          if (addedNodes.has(rangeType)) {
            // For slot_usage, append "(usage)" to semantic label
            const semanticLabel = getSemanticLabel(slotName);
            addLink(className, rangeType, 'aggregation', `${semanticLabel} (usage)`, false);
          }
        }
        // Annotation-based relationships (dual-class pattern)
        // linked_collection_type - custodian type → collection type
        if (importInfo.linkedCollectionType && addedNodes.has(importInfo.linkedCollectionType)) {
          addLink(className, importInfo.linkedCollectionType, 'association', 'linked_collection_type', false);
        }
        // linked_custodian_type - collection type → custodian type
        if (importInfo.linkedCustodianType && addedNodes.has(importInfo.linkedCustodianType)) {
          addLink(className, importInfo.linkedCustodianType, 'association', 'linked_custodian_type', false);
        }
      }
      // Also check parentClasses map for inheritance
      if (parentClasses[className] && addedNodes.has(parentClasses[className])) {
        addLink(className, parentClasses[className], 'inheritance', 'is_a', false);
      }
    }
    
    // EXPORTS: Show what references this class
    if (showExports) {
      const exportInfo = allExportInfo[className];
      if (exportInfo) {
        // Subclasses - arrow points from subclass to this class
        for (const subclass of exportInfo.subclasses) {
          if (addedNodes.has(subclass)) {
            addLink(subclass, className, 'inheritance', 'is_a', false);
          }
        }
        // Mixin users - arrow points from user to this class
        for (const mixinUser of exportInfo.mixinUsers) {
          if (addedNodes.has(mixinUser)) {
            addLink(mixinUser, className, 'association', 'mixin', false);
          }
        }
        // Classes using slots with this range (SLOT EDGE - use semantic predicate)
        for (const { className: usingClass, slotName } of exportInfo.classesUsingSlotWithThisRange) {
          if (addedNodes.has(usingClass)) {
            addLink(usingClass, className, 'aggregation', slotName, true);
          }
        }
        // Classes referencing in slot_usage
        for (const refClass of exportInfo.classesReferencingInSlotUsage) {
          if (addedNodes.has(refClass)) {
            addLink(refClass, className, 'association', 'slot_usage', false);
          }
        }
        // Annotation-based relationships (dual-class pattern)
        // Collection types that have linked_custodian_type pointing to this class
        for (const collectionType of exportInfo.linkedCollectionTypes) {
          if (addedNodes.has(collectionType)) {
            addLink(collectionType, className, 'association', 'linked_custodian_type', false);
          }
        }
      }
    }
  };
  
  // BFS to add nodes up to specified depth
  const queue: Array<{ className: string; currentDepth: number }> = [
    { className: centerClassName, currentDepth: 0 }
  ];
  
  while (queue.length > 0) {
    const { className, currentDepth } = queue.shift()!;
    
    // Skip if already processed at same or lower depth
    const existingDepth = processedAtDepth.get(className);
    if (existingDepth !== undefined && existingDepth <= currentDepth) {
      continue;
    }
    processedAtDepth.set(className, currentDepth);
    
    // Add node
    addNode(className, currentDepth);
    
    // If we haven't reached max depth, queue related classes
    if (currentDepth < depth) {
      const related = getRelatedClasses(className);
      for (const relatedClass of related) {
        // Only queue if not already processed at a lower depth
        const relDepth = processedAtDepth.get(relatedClass);
        if (relDepth === undefined || relDepth > currentDepth + 1) {
          queue.push({ className: relatedClass, currentDepth: currentDepth + 1 });
        }
      }
    }
  }
  
  // Now add all links between added nodes
  for (const className of addedNodes) {
    addRelationshipsFromClass(className);
  }
  
  // Debug logging to understand link generation
  debugLog(`[buildFilteredUMLDiagram] Building diagram for ${centerClassName}, depth=${depth}`);
  debugLog(`[buildFilteredUMLDiagram] showImports=${showImports}, showExports=${showExports}`);
  debugLog(`[buildFilteredUMLDiagram] Total nodes: ${nodes.length}`, nodes.map(n => n.name));
  debugLog(`[buildFilteredUMLDiagram] Total links: ${links.length}`);
  
  // Log link breakdown by type
  const linksByType = {
    inheritance: links.filter(l => l.type === 'inheritance'),
    aggregation: links.filter(l => l.type === 'aggregation'),
    association: links.filter(l => l.type === 'association'),
    composition: links.filter(l => l.type === 'composition'),
  };
  debugLog(`[buildFilteredUMLDiagram] Links by type:`, {
    inheritance: linksByType.inheritance.length,
    aggregation: linksByType.aggregation.length,
    association: linksByType.association.length,
    composition: linksByType.composition.length,
  });
  
  // Log the actual import info to understand why slot ranges might be missing
  const centerImportInfo = allImportInfo[centerClassName];
  if (centerImportInfo) {
    debugLog(`[buildFilteredUMLDiagram] Center class import info:`, {
      parentClass: centerImportInfo.parentClass,
      mixins: centerImportInfo.mixins,
      slotRanges: centerImportInfo.slotRanges,
      slotUsageRanges: centerImportInfo.slotUsageRanges,
      linkedCollectionType: centerImportInfo.linkedCollectionType,
      linkedCustodianType: centerImportInfo.linkedCustodianType,
    });
  } else {
    debugLog(`[buildFilteredUMLDiagram] WARNING: No import info for center class ${centerClassName}`);
  }
  
  return {
    nodes,
    links,
    title: `${centerClassName} Relationships (Depth: ${depth})`,
  };
};

// Bilingual text content
const TEXT = {
  sidebarTitle: { nl: 'LinkML-schema\'s', en: 'LinkML Schemas' },
  pageTitle: { nl: 'LinkML-schemaviewer', en: 'LinkML Schema Viewer' },
  visualView: { nl: 'Visuele weergave', en: 'Visual View' },
  rawYaml: { nl: 'Ruwe YAML', en: 'Raw YAML' },
  loading: { nl: 'Schema laden...', en: 'Loading schema...' },
  noSchemasFound: { nl: 'Geen schema\'s gevonden. Zorg dat het manifest is gegenereerd.', en: 'No schemas found. Make sure the manifest is generated.' },
  failedToInit: { nl: 'Initialiseren van schemalijst mislukt', en: 'Failed to initialize schema list' },
  failedToLoad: { nl: 'Schema laden mislukt:', en: 'Failed to load schema:' },
  unnamedSchema: { nl: 'Naamloos schema', en: 'Unnamed Schema' },
  prefixes: { nl: 'Prefixen', en: 'Prefixes' },
  classes: { nl: 'Klassen', en: 'Classes' },
  slots: { nl: 'Slots', en: 'Slots' },
  enumerations: { nl: 'Enumeraties', en: 'Enumerations' },
  imports: { nl: 'Imports', en: 'Imports' },
  abstract: { nl: 'abstract', en: 'abstract' },
  required: { nl: 'verplicht', en: 'required' },
  multivalued: { nl: 'meervoudig', en: 'multivalued' },
  uri: { nl: 'URI:', en: 'URI:' },
  id: { nl: 'ID:', en: 'ID:' },
  version: { nl: 'Versie:', en: 'Version:' },
  range: { nl: 'Bereik:', en: 'Range:' },
  pattern: { nl: 'Patroon:', en: 'Pattern:' },
  slotsLabel: { nl: 'Slots:', en: 'Slots:' },
  exactMappings: { nl: 'Exacte mappings:', en: 'Exact Mappings:' },
  closeMappings: { nl: 'Vergelijkbare mappings:', en: 'Close Mappings:' },
  permissibleValues: { nl: 'Toegestane waarden:', en: 'Permissible Values:' },
  meaning: { nl: 'betekenis:', en: 'meaning:' },
  searchPlaceholder: { nl: 'Zoeken in waarden...', en: 'Search values...' },
  showAll: { nl: 'Alles tonen', en: 'Show all' },
  showLess: { nl: 'Minder tonen', en: 'Show less' },
  showing: { nl: 'Toont', en: 'Showing' },
  of: { nl: 'van', en: 'of' },
  noResults: { nl: 'Geen resultaten gevonden', en: 'No results found' },
  searchSchemas: { nl: 'Zoeken in schema\'s...', en: 'Search schemas...' },
  mainSchema: { nl: 'Hoofdschema', en: 'Main Schema' },
  noMatchingSchemas: { nl: 'Geen overeenkomende schema\'s', en: 'No matching schemas' },
  copyToClipboard: { nl: 'Kopieer naar klembord', en: 'Copy to clipboard' },
  copied: { nl: 'Gekopieerd!', en: 'Copied!' },
  use3DPolygon: { nl: '3D-polygoon', en: '3D Polygon' },
  use2DBadge: { nl: '2D-badge', en: '2D Badge' },
  // Class-level imports (dependencies)
  classImports: { nl: 'Imports', en: 'Imports' },
  classImportsTooltip: { 
    nl: 'Toont welke klassen en types deze klasse als afhankelijkheden gebruikt (ouderklasse, mixins, slot ranges)', 
    en: 'Shows what classes and types this class depends on (parent class, mixins, slot ranges)' 
  },
  parentClassLabel: { nl: 'Ouderklasse (is_a)', en: 'Parent Class (is_a)' },
  mixinsLabel: { nl: 'Mixins', en: 'Mixins' },
  slotRangesLabel: { nl: 'Slot Ranges', en: 'Slot Ranges' },
  slotUsageRangesLabel: { nl: 'Slot Usage Ranges', en: 'Slot Usage Ranges' },
  noImports: { nl: 'Deze klasse heeft geen afhankelijkheden van andere klassen.', en: 'This class has no dependencies on other classes.' },
  // File-level imports tooltip (to differentiate from class-level)
  fileImportsTooltip: { 
    nl: 'Toont welke LinkML-modules dit schemabestand importeert op bestandsniveau', 
    en: 'Shows which LinkML modules this schema file imports at file level' 
  },
  // UML diagram direction toggles
  umlShowImports: { nl: 'Imports tonen', en: 'Show Imports' },
  umlShowExports: { nl: 'Exports tonen', en: 'Show Exports' },
  umlImportsTooltip: { nl: 'Toon afhankelijkheden (ouderklasse, mixins, slot ranges)', en: 'Show dependencies (parent class, mixins, slot ranges)' },
  umlExportsTooltip: { nl: 'Toon verwijzingen naar deze klasse (subklassen, mixin-gebruikers)', en: 'Show references to this class (subclasses, mixin users)' },
  umlNoRelationshipsShown: { 
    nl: 'Selecteer minimaal één richting (Imports of Exports) om relaties te tonen.', 
    en: 'Select at least one direction (Imports or Exports) to show relationships.' 
  },
  umlNoRelationshipsFound: { 
    nl: 'Deze klasse heeft geen relaties in de geselecteerde richting(en).', 
    en: 'This class has no relationships in the selected direction(s).' 
  },
  // Sidebar collapse
  collapseSidebar: { nl: 'Zijbalk inklappen', en: 'Collapse sidebar' },
  expandSidebar: { nl: 'Zijbalk uitklappen', en: 'Expand sidebar' },
  // Class slots accordion
  classSlots: { nl: 'Slots', en: 'Slots' },
  classSlotsTooltip: { 
    nl: 'Toont alle slots (eigenschappen) die gedefinieerd zijn voor deze klasse', 
    en: 'Shows all slots (properties) defined for this class' 
  },
  noClassSlots: { nl: 'Deze klasse heeft geen slots.', en: 'This class has no slots.' },
  // Slot details - semantic URI and mappings
  slotUri: { nl: 'Slot URI:', en: 'Slot URI:' },
  identifier: { nl: 'identifier', en: 'identifier' },
  examples: { nl: 'Voorbeelden:', en: 'Examples:' },
  narrowMappings: { nl: 'Specifiekere mappings:', en: 'Narrow Mappings:' },
  broadMappings: { nl: 'Bredere mappings:', en: 'Broad Mappings:' },
  relatedMappings: { nl: 'Gerelateerde mappings:', en: 'Related Mappings:' },
  comments: { nl: 'Opmerkingen:', en: 'Comments:' },
  // Slot usage indicator
  slotUsageBadge: { nl: 'slot_usage', en: 'slot_usage' },
  slotUsageTooltip: { 
    nl: 'Deze slot heeft klasse-specifieke overschrijvingen gedefinieerd in slot_usage. Eigenschappen met ✦ komen uit de slot_usage, andere zijn overgenomen van de generieke slot-definitie.', 
    en: 'This slot has class-specific overrides defined in slot_usage. Properties marked with ✦ come from slot_usage, others are inherited from the generic slot definition.' 
  },
  slotUsageOverrideMarker: { nl: '✦', en: '✦' },
  inheritedFromGeneric: { nl: 'overgenomen', en: 'inherited' },
  overriddenInSlotUsage: { nl: 'overschreven in slot_usage', en: 'overridden in slot_usage' },
  // Slot usage legend
  slotUsageLegendTitle: { nl: 'Legenda slot_usage', en: 'slot_usage Legend' },
  slotUsageLegendToggle: { nl: 'Legenda', en: 'Legend' },
  slotUsageLegendBadge: { 
    nl: 'Slots met dit badge hebben klasse-specifieke overschrijvingen', 
    en: 'Slots with this badge have class-specific overrides' 
  },
  slotUsageLegendMarker: { 
    nl: 'Dit symbool verschijnt vóór eigenschappen die overschreven zijn in slot_usage', 
    en: 'This symbol appears before properties overridden in slot_usage' 
  },
  slotUsageLegendBorder: { 
    nl: 'Groene linkerkant geeft aan dat de slot slot_usage overschrijvingen heeft', 
    en: 'Green left border indicates the slot has slot_usage overrides' 
  },
  slotUsageLegendInherited: { 
    nl: 'Eigenschappen zonder ✦ zijn overgenomen van de generieke slot-definitie', 
    en: 'Properties without ✦ are inherited from the generic slot definition' 
  },
  // Slot usage comparison view
  slotUsageCompareToggle: { nl: 'Vergelijk', en: 'Compare' },
  slotUsageCompareTooltip: { 
    nl: 'Toon generieke definitie naast slot_usage overschrijvingen', 
    en: 'Show generic definition alongside slot_usage overrides' 
  },
  slotUsageCompareGeneric: { nl: 'Generieke Definitie', en: 'Generic Definition' },
  slotUsageCompareOverride: { nl: 'slot_usage Overschrijving', en: 'slot_usage Override' },
  slotUsageCompareDiff: { nl: 'gewijzigd', en: 'changed' },
  slotUsageCompareInherited: { nl: '(overgenomen)', en: '(inherited)' },
  slotUsageCompareNotDefined: { nl: '(niet gedefinieerd)', en: '(not defined)' },
};

// Dynamically discover schema files from the modules directory
interface SchemaCategory {
  name: string;
  displayName: string;
  files: SchemaFile[];
}

const LinkMLViewerPage: React.FC = () => {
  const { language } = useLanguage();
  const t = (key: keyof typeof TEXT) => TEXT[key][language];
  
  const [searchParams, setSearchParams] = useSearchParams();
  const [categories, setCategories] = useState<SchemaCategory[]>([]);
  const [selectedSchema, setSelectedSchema] = useState<SchemaFile | null>(null);
  const [schema, setSchema] = useState<LinkMLSchema | null>(null);
  const [rawYaml, setRawYaml] = useState<string | null>(null);
  const [viewMode, setViewMode] = useState<'visual' | 'raw'>('visual');
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['classes', 'enums', 'slots']));
  const [highlightedClass, setHighlightedClass] = useState<string | null>(null);
  const highlightedRef = useRef<HTMLDivElement>(null);
  
  // State for expandable enum ranges in slots
  const [expandedEnumRanges, setExpandedEnumRanges] = useState<Set<string>>(new Set());
  const [loadedEnums, setLoadedEnums] = useState<Record<string, LinkMLEnum | null>>({});
  const [enumSearchFilters, setEnumSearchFilters] = useState<Record<string, string>>({});
  const [enumShowAll, setEnumShowAll] = useState<Record<string, boolean>>({});
  
  // State for pre-loaded custodian types (loaded async from schema annotations)
  // Maps element name -> custodian type codes
  const [classCustodianTypes, setClassCustodianTypes] = useState<Record<string, CustodianTypeCode[]>>({});
  const [slotCustodianTypes, setSlotCustodianTypes] = useState<Record<string, CustodianTypeCode[]>>({});
  const [enumCustodianTypes, setEnumCustodianTypes] = useState<Record<string, CustodianTypeCode[]>>({});
  const [_custodianTypesLoaded, setCustodianTypesLoaded] = useState(false);
  
  // State for sidebar search and category filters
  const [sidebarSearch, setSidebarSearch] = useState<string>('');
  const [categoryFilters, setCategoryFilters] = useState<Record<string, boolean>>({
    main: true,
    class: true,
    enum: true,
    slot: true,
  });
  
  // State for sidebar collapse
  const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
  
  // State for copy to clipboard feedback
  const [copyFeedback, setCopyFeedback] = useState(false);
  
  // State for expandable Exports section in class details
  const [expandedExports, setExpandedExports] = useState<Set<string>>(new Set());
  const [classExports, setClassExports] = useState<Record<string, ClassExportInfo>>({});
  const [loadingExports, setLoadingExports] = useState<Set<string>>(new Set());
  
  // State for expandable Imports section in class details (dependencies)
  const [expandedImports, setExpandedImports] = useState<Set<string>>(new Set());
  const [classImports, setClassImports] = useState<Record<string, ClassImportInfo>>({});
  const [loadingImports, setLoadingImports] = useState<Set<string>>(new Set());
  
  // State for expandable UML diagram section in class details
  const [expandedUML, setExpandedUML] = useState<Set<string>>(new Set());
  
  // State for expandable class slots section in class details
  const [expandedClassSlots, setExpandedClassSlots] = useState<Set<string>>(new Set());
  
  // State for pre-loaded dependency counts (imports/exports) for all classes
  // This allows showing counts immediately without loading full import/export data
  const [dependencyCounts, setDependencyCounts] = useState<Map<string, ClassDependencyCounts>>(new Map());
  
  // State for UML depth slider per class (1 = direct relationships only, 2 = secondary, etc.)
  const [umlDepth, setUmlDepth] = useState<Record<string, number>>({});
  
  // State for UML direction toggles per class (show imports, exports, or both)
  // Default: both enabled (true)
  const [umlShowImports, setUmlShowImports] = useState<Record<string, boolean>>({});
  const [umlShowExports, setUmlShowExports] = useState<Record<string, boolean>>({});
  
  // State for UML layout type per class
  const [umlLayoutType, setUmlLayoutType] = useState<Record<string, 'dagre' | 'force' | 'circular' | 'radial'>>({});
  const [umlDagreDirection, setUmlDagreDirection] = useState<Record<string, 'TB' | 'LR'>>({});
  
  // State for UML fullscreen mode per class
  const [umlFullscreen, setUmlFullscreen] = useState<string | null>(null);
  
  // State for layout dropdown open per class
  const [umlLayoutDropdownOpen, setUmlLayoutDropdownOpen] = useState<Record<string, boolean>>({});
  
  // State for parent class mapping (className -> parentClassName from is_a inheritance)
  // Built incrementally as classes are loaded for UML diagrams
  const [parentClassMap, setParentClassMap] = useState<Record<string, string>>({});
  
  // State for service-loaded slot definitions (from getAllSlots)
  // Used to resolve slot names to full definitions in class details
  const [serviceSlots, setServiceSlots] = useState<Map<string, SlotDefinition>>(new Map());
  
  // State for slot semantic labels (slot name -> semantic predicate like "schema:collection")
  // Computed from serviceSlots when available
  const [slotSemanticLabels, setSlotSemanticLabels] = useState<Map<string, string>>(new Map());
  
  // State for 3D polygon indicator toggle
  // Persisted in localStorage - defaults to false (2D SVG) if not set
  const [use3DIndicator, setUse3DIndicator] = useState(() => {
    try {
      const saved = localStorage.getItem('linkml-viewer-use-3d');
      return saved === 'true';
    } catch {
      return false;
    }
  });
  
  // Persist 3D mode preference to localStorage
  useEffect(() => {
    try {
      localStorage.setItem('linkml-viewer-use-3d', use3DIndicator ? 'true' : 'false');
    } catch {
      // Ignore localStorage errors (e.g., private browsing)
    }
  }, [use3DIndicator]);
  
  // State for bidirectional hover sync between 3D polyhedrons and legend bar
  const [hoveredCustodianType, setHoveredCustodianType] = useState<CustodianTypeCode | null>(null);
  
  // State for slot_usage legend visibility toggle
  const [showSlotUsageLegend, setShowSlotUsageLegend] = useState(false);
  
  // State for slot_usage comparison view - tracks which slots have comparison expanded
  // Key format: "className:slotName"
  const [expandedSlotComparisons, setExpandedSlotComparisons] = useState<Set<string>>(new Set());
  
  // Toggle comparison view for a specific slot
  const toggleSlotComparison = (className: string, slotName: string) => {
    const key = `${className}:${slotName}`;
    setExpandedSlotComparisons(prev => {
      const next = new Set(prev);
      if (next.has(key)) {
        next.delete(key);
      } else {
        next.add(key);
      }
      return next;
    });
  };
  
  // State for filtering schema elements by custodian type (click on polyhedron face or legend)
  // Multi-select: Set of selected type codes (empty = no filter)
  // Initialize from URL params (e.g., ?custodian=G,L,A)
  const [custodianTypeFilter, setCustodianTypeFilter] = useState<Set<CustodianTypeCode>>(() => {
    const param = searchParams.get('custodian');
    if (!param) return new Set();
    const codes = param.split(',').filter((c): c is CustodianTypeCode => 
      CUSTODIAN_TYPE_CODES.includes(c as CustodianTypeCode)
    );
    return new Set(codes);
  });
  
  // Sync custodian filter to URL params
  useEffect(() => {
    const currentParam = searchParams.get('custodian');
    const filterCodes = Array.from(custodianTypeFilter).sort().join(',');
    
    // Only update if different to avoid loops
    if (custodianTypeFilter.size === 0 && currentParam) {
      // Remove param if filter is empty
      searchParams.delete('custodian');
      setSearchParams(searchParams, { replace: true });
    } else if (custodianTypeFilter.size > 0 && currentParam !== filterCodes) {
      searchParams.set('custodian', filterCodes);
      setSearchParams(searchParams, { replace: true });
    }
  }, [custodianTypeFilter, searchParams, setSearchParams]);
  
  // Ref for main content (used by navigation-synced collapsible header)
  const mainContentRef = useRef<HTMLElement>(null);
  
  // Schema loading progress tracking
  const { progress: schemaProgress, isLoading: isSchemaServiceLoading, isComplete: isSchemaServiceComplete } = useSchemaLoadingProgress();
  
  // Handler for filtering by custodian type (clicking polyhedron face or legend item)
  // Multi-select toggle behavior: clicking type adds/removes from set
  const handleCustodianTypeFilter = useCallback((typeCode: CustodianTypeCode) => {
    setCustodianTypeFilter(prev => {
      const newSet = new Set(prev);
      if (newSet.has(typeCode)) {
        newSet.delete(typeCode);
      } else {
        newSet.add(typeCode);
      }
      return newSet;
    });
  }, []);

  // Toggle exports section for a class and load export data on demand
  const toggleExports = useCallback(async (className: string) => {
    // Toggle expansion state
    setExpandedExports(prev => {
      const next = new Set(prev);
      if (next.has(className)) {
        next.delete(className);
      } else {
        next.add(className);
      }
      return next;
    });
    
    // Load export data if not already loaded and schema service is ready
    if (!classExports[className] && !loadingExports.has(className) && isSchemaServiceComplete) {
      setLoadingExports(prev => new Set(prev).add(className));
      try {
        const exportInfo = await linkmlSchemaService.getClassExportInfo(className);
        setClassExports(prev => ({ ...prev, [className]: exportInfo }));
      } catch (error) {
        console.error(`Error loading export info for ${className}:`, error);
      } finally {
        setLoadingExports(prev => {
          const next = new Set(prev);
          next.delete(className);
          return next;
        });
      }
    }
  }, [classExports, loadingExports, isSchemaServiceComplete]);

  // Toggle imports section for a class and load import data on demand
  const toggleImports = useCallback(async (className: string) => {
    // Toggle expansion state
    setExpandedImports(prev => {
      const next = new Set(prev);
      if (next.has(className)) {
        next.delete(className);
      } else {
        next.add(className);
      }
      return next;
    });
    
    // Load import data if not already loaded and schema service is ready
    if (!classImports[className] && !loadingImports.has(className) && isSchemaServiceComplete) {
      setLoadingImports(prev => new Set(prev).add(className));
      try {
        const importInfo = await linkmlSchemaService.getClassImportInfo(className);
        setClassImports(prev => ({ ...prev, [className]: importInfo }));
      } catch (error) {
        console.error(`Error loading import info for ${className}:`, error);
      } finally {
        setLoadingImports(prev => {
          const next = new Set(prev);
          next.delete(className);
          return next;
        });
      }
    }
  }, [classImports, loadingImports, isSchemaServiceComplete]);

  // Toggle UML diagram section for a class
  // Loads both exports AND imports data since UML diagram can show both directions
  const toggleUML = useCallback(async (className: string) => {
    setExpandedUML(prev => {
      const next = new Set(prev);
      if (next.has(className)) {
        next.delete(className);
      } else {
        next.add(className);
      }
      return next;
    });
    
    // Load exports data if not already loaded (for "Show Exports" checkbox)
    if (!classExports[className] && !loadingExports.has(className) && isSchemaServiceComplete) {
      setLoadingExports(prev => new Set(prev).add(className));
      try {
        const exportInfo = await linkmlSchemaService.getClassExportInfo(className);
        setClassExports(prev => ({ ...prev, [className]: exportInfo }));
      } catch (error) {
        console.error(`Error loading export info for ${className}:`, error);
      } finally {
        setLoadingExports(prev => {
          const next = new Set(prev);
          next.delete(className);
          return next;
        });
      }
    }
    
    // Load imports data if not already loaded (for "Show Imports" checkbox)
    if (!classImports[className] && !loadingImports.has(className) && isSchemaServiceComplete) {
      setLoadingImports(prev => new Set(prev).add(className));
      try {
        const importInfo = await linkmlSchemaService.getClassImportInfo(className);
        setClassImports(prev => ({ ...prev, [className]: importInfo }));
      } catch (error) {
        console.error(`Error loading import info for ${className}:`, error);
      } finally {
        setLoadingImports(prev => {
          const next = new Set(prev);
          next.delete(className);
          return next;
        });
      }
    }
  }, [classExports, classImports, loadingExports, loadingImports, isSchemaServiceComplete]);

  // Toggle class slots accordion section
  const toggleClassSlots = useCallback((className: string) => {
    setExpandedClassSlots(prev => {
      const next = new Set(prev);
      if (next.has(className)) {
        next.delete(className);
      } else {
        next.add(className);
      }
      return next;
    });
  }, []);

  // Load export AND import info for related classes when increasing depth
  // Uses BFS to find all classes within the specified depth and loads their data
  const loadRelationshipInfoForDepth = useCallback(async (centerClassName: string, depth: number) => {
    if (!isSchemaServiceComplete) return;
    
    // Get currently loaded info for center class
    const centerExport = classExports[centerClassName];
    const centerImport = classImports[centerClassName];
    if (!centerExport && !centerImport) return;
    
    // BFS to find all classes we need to load
    const classesToLoadExports = new Set<string>();
    const classesToLoadImports = new Set<string>();
    const visited = new Set<string>();
    const queue: Array<{ className: string; currentDepth: number }> = [
      { className: centerClassName, currentDepth: 0 }
    ];
    
    while (queue.length > 0) {
      const { className, currentDepth } = queue.shift()!;
      
      if (visited.has(className) || currentDepth > depth) continue;
      visited.add(className);
      
      const exportInfo = classExports[className];
      const importInfo = classImports[className];
      
      // Mark for loading if not already loaded
      if (!exportInfo && !loadingExports.has(className)) {
        classesToLoadExports.add(className);
      }
      if (!importInfo && !loadingImports.has(className)) {
        classesToLoadImports.add(className);
      }
      
      // Get related classes from exports (reverse dependencies)
      const related: string[] = [];
      if (exportInfo) {
        related.push(...exportInfo.subclasses);
        related.push(...exportInfo.mixinUsers);
        related.push(...exportInfo.classesUsingSlotWithThisRange.map(c => c.className));
        related.push(...exportInfo.classesReferencingInSlotUsage);
        // Annotation-based relationships (dual-class pattern)
        related.push(...exportInfo.linkedCollectionTypes);
      }
      
      // Get related classes from imports (forward dependencies)
      if (importInfo) {
        if (importInfo.parentClass) related.push(importInfo.parentClass);
        related.push(...importInfo.mixins);
        related.push(...importInfo.slotRanges.map(r => r.rangeType));
        related.push(...importInfo.slotUsageRanges.map(r => r.rangeType));
        // Annotation-based relationships (dual-class pattern)
        if (importInfo.linkedCollectionType) related.push(importInfo.linkedCollectionType);
        if (importInfo.linkedCustodianType) related.push(importInfo.linkedCustodianType);
      }
      
      // Also check parent class from semantic info if available
      const parentClass = parentClassMap[className];
      if (parentClass) {
        related.push(parentClass);
      }
      
      // Queue related classes for next depth level
      if (currentDepth < depth) {
        for (const relatedClass of related) {
          if (!visited.has(relatedClass)) {
            queue.push({ className: relatedClass, currentDepth: currentDepth + 1 });
          }
        }
      }
    }
    
    // Load all classes that need loading
    const hasExportsToLoad = classesToLoadExports.size > 0;
    const hasImportsToLoad = classesToLoadImports.size > 0;
    if (!hasExportsToLoad && !hasImportsToLoad) return;
    
    // Mark as loading
    if (hasExportsToLoad) {
      setLoadingExports(prev => {
        const next = new Set(prev);
        classesToLoadExports.forEach(c => next.add(c));
        return next;
      });
    }
    if (hasImportsToLoad) {
      setLoadingImports(prev => {
        const next = new Set(prev);
        classesToLoadImports.forEach(c => next.add(c));
        return next;
      });
    }
    
    // Load exports in parallel
    const exportPromises = Array.from(classesToLoadExports).map(async (className) => {
      try {
        const exportInfo = await linkmlSchemaService.getClassExportInfo(className);
        
        // Also try to get parent class info
        try {
          const semanticInfo = await linkmlSchemaService.getClassSemanticInfo(className);
          if (semanticInfo?.parentClass) {
            setParentClassMap(prev => ({ ...prev, [className]: semanticInfo.parentClass! }));
          }
        } catch {
          // Semantic info not available for all classes
        }
        
        return { className, exportInfo };
      } catch (error) {
        console.error(`Error loading export info for ${className}:`, error);
        return { className, exportInfo: null };
      }
    });
    
    // Load imports in parallel
    const importPromises = Array.from(classesToLoadImports).map(async (className) => {
      try {
        const importInfo = await linkmlSchemaService.getClassImportInfo(className);
        return { className, importInfo };
      } catch (error) {
        console.error(`Error loading import info for ${className}:`, error);
        return { className, importInfo: null };
      }
    });
    
    const [exportResults, importResults] = await Promise.all([
      Promise.all(exportPromises),
      Promise.all(importPromises)
    ]);
    
    // Update state with loaded export info
    if (hasExportsToLoad) {
      setClassExports(prev => {
        const next = { ...prev };
        for (const { className, exportInfo } of exportResults) {
          if (exportInfo) {
            next[className] = exportInfo;
          }
        }
        return next;
      });
      
      // Clear loading state
      setLoadingExports(prev => {
        const next = new Set(prev);
        classesToLoadExports.forEach(c => next.delete(c));
        return next;
      });
    }
    
    // Update state with loaded import info
    if (hasImportsToLoad) {
      setClassImports(prev => {
        const next = { ...prev };
        for (const { className, importInfo } of importResults) {
          if (importInfo) {
            next[className] = importInfo;
          }
        }
        return next;
      });
      
      // Clear loading state
      setLoadingImports(prev => {
        const next = new Set(prev);
        classesToLoadImports.forEach(c => next.delete(c));
        return next;
      });
    }
    
    // If we loaded new classes, we may need to traverse further
    // Schedule another pass if we're not at full depth yet
    const hasNewExports = exportResults.some(r => r.exportInfo !== null);
    const hasNewImports = importResults.some(r => r.importInfo !== null);
    if ((hasNewExports || hasNewImports) && depth > 1) {
      // Re-run to potentially load more classes at deeper levels
      setTimeout(() => loadRelationshipInfoForDepth(centerClassName, depth), 100);
    }
  }, [classExports, classImports, loadingExports, loadingImports, parentClassMap, isSchemaServiceComplete]);

  // Debounced version of loadRelationshipInfoForDepth for slider interaction
  // This prevents excessive API calls when dragging the slider
  const debouncedLoadRelationshipInfo = useMemo(
    () => debounce((className: string, depth: number) => {
      loadRelationshipInfoForDepth(className, depth);
    }, 300),
    [loadRelationshipInfoForDepth]
  );

  // Cleanup debounced function on unmount
  useEffect(() => {
    return () => {
      debouncedLoadRelationshipInfo.cancel();
    };
  }, [debouncedLoadRelationshipInfo]);

  // Create a lookup map from slot names to slot definitions
  // This allows renderClassDetails to resolve slot names to full slot objects
  // Priority: service slots (comprehensive) > local schema slots (fallback)
  const slotLookupMap = useMemo(() => {
    const map = new Map<string, LinkMLSlot>();
    
    // First, add any slots from the local schema file (fallback)
    if (schema) {
      const localSlots = extractSlots(schema);
      localSlots.forEach(slot => map.set(slot.name, slot));
    }
    
    // Then, add/override with service slots (comprehensive - includes all modules)
    // SlotDefinition is compatible with LinkMLSlot (superset)
    serviceSlots.forEach((slot, name) => {
      map.set(name, slot as LinkMLSlot);
    });
    
    return map;
  }, [schema, serviceSlots]);

  // Cache for memoized UML diagrams - keyed by className:depth:showImports:showExports
  // This prevents expensive diagram rebuilding when only unrelated state changes
  const diagramCache = useRef<Map<string, UMLDiagram>>(new Map());
  const diagramCacheDeps = useRef<{ classExports: typeof classExports; classImports: typeof classImports; parentClassMap: typeof parentClassMap; slotSemanticLabels: typeof slotSemanticLabels } | null>(null);
  
  // Clear cache when dependencies change
  if (diagramCacheDeps.current === null ||
      diagramCacheDeps.current.classExports !== classExports ||
      diagramCacheDeps.current.classImports !== classImports ||
      diagramCacheDeps.current.parentClassMap !== parentClassMap ||
      diagramCacheDeps.current.slotSemanticLabels !== slotSemanticLabels) {
    diagramCache.current.clear();
    diagramCacheDeps.current = { classExports, classImports, parentClassMap, slotSemanticLabels };
  }
  
  // Memoized UML diagram builder with result caching to prevent recalculation
  // Returns cached result if same parameters, otherwise builds and caches new diagram
  const getMemoizedUMLDiagram = useCallback((className: string, depth: number, showImports: boolean, showExports: boolean): UMLDiagram => {
    const cacheKey = `${className}:${depth}:${showImports}:${showExports}`;
    
    // Return cached diagram if available
    const cached = diagramCache.current.get(cacheKey);
    if (cached) {
      return cached;
    }
    
    // Build new diagram and cache it (pass slot semantic labels for edge predicates)
    const diagram = buildFilteredUMLDiagram(className, classExports, classImports, depth, parentClassMap, showImports, showExports, slotSemanticLabels);
    diagramCache.current.set(cacheKey, diagram);
    
    // Limit cache size to prevent memory issues (keep last 20 diagrams)
    if (diagramCache.current.size > 20) {
      const firstKey = diagramCache.current.keys().next().value;
      if (firstKey) diagramCache.current.delete(firstKey);
    }
    
    return diagram;
  }, [classExports, classImports, parentClassMap, slotSemanticLabels]);

  // Navigate to a class by updating URL params and selecting the schema file
  const navigateToClass = useCallback((className: string) => {
    setSearchParams({ class: className });
    setHighlightedClass(className);
    
    // Find and select the class file
    const classFile = categories
      .find(cat => cat.name === 'class')
      ?.files.find(file => file.name === className);
    
    if (classFile) {
      setSelectedSchema(classFile);
      // Ensure classes section is expanded
      setExpandedSections(prev => new Set([...prev, 'classes']));
    }
  }, [categories, setSearchParams]);

  // Track if initialization has already happened (prevents re-init on URL param changes)
  const isInitializedRef = useRef(false);
  
  // Handle URL parameters for deep linking (only used on initial mount)
  const handleUrlParams = useCallback((cats: SchemaCategory[], currentSearchParams: URLSearchParams) => {
    const classParam = currentSearchParams.get('class');
    
    if (classParam) {
      setHighlightedClass(classParam);
      
      // Find the schema file that contains this class
      const classFile = cats
        .find(cat => cat.name === 'class')
        ?.files.find(file => file.name === classParam);
      
      if (classFile) {
        setSelectedSchema(classFile);
        // Ensure classes section is expanded
        setExpandedSections(prev => new Set([...prev, 'classes']));
      }
    }
  }, []);

  // Initialize schema file list from manifest - RUNS ONLY ONCE on mount
  // Note: Does NOT depend on searchParams to prevent re-initialization when
  // custodian filter changes the URL. Deep linking for ?class= is handled
  // by reading searchParams directly inside the effect on initial mount only.
  useEffect(() => {
    // Skip if already initialized (prevents re-init on searchParams changes from filter)
    if (isInitializedRef.current) {
      return;
    }
    
    const initializeSchemas = async () => {
      setIsLoading(true);
      try {
        // Fetch the schema manifest (dynamically generated at build time)
        const cats = await fetchSchemaManifest();
        
        if (cats.length === 0) {
          setError(t('noSchemasFound'));
          return;
        }
        
        setCategories(cats);
        
        // Check URL params for deep linking (read searchParams directly, don't depend on it)
        handleUrlParams(cats, searchParams);
        
        // Select main schema by default if no URL param set the schema
        const classParam = searchParams.get('class');
        if (!classParam && cats[0]?.files.length > 0) {
          setSelectedSchema(cats[0].files[0]);
        }
        
        // Mark as initialized to prevent re-running
        isInitializedRef.current = true;
      } catch (err) {
        setError(t('failedToInit'));
        console.error(err);
      } finally {
        setIsLoading(false);
      }
    };
    
    initializeSchemas();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // Empty deps - run only on mount

  // Scroll to highlighted class when it changes
  useEffect(() => {
    if (highlightedClass && highlightedRef.current) {
      setTimeout(() => {
        highlightedRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }, 100);
    }
  }, [highlightedClass, schema]);

  // Fetch schema manifest from dynamically generated JSON file
  const fetchSchemaManifest = async (): Promise<SchemaCategory[]> => {
    try {
      const response = await fetch('/schemas/20251121/linkml/manifest.json');
      if (!response.ok) {
        throw new Error(`Failed to fetch manifest: ${response.status}`);
      }
      const manifest = await response.json();
      
      // Transform manifest categories to our format
      return manifest.categories.map((cat: { name: string; displayName: string; files: Array<{ name: string; path: string; category: string }> }) => ({
        name: cat.name,
        displayName: cat.displayName,
        files: cat.files.map((file: { name: string; path: string; category: string }) => ({
          name: file.name,
          path: file.path,
          category: cat.name as SchemaFile['category']
        }))
      }));
    } catch (err) {
      console.error('Failed to fetch schema manifest:', err);
      // Return empty categories on error
      return [];
    }
  };

  // Load selected schema
  useEffect(() => {
    if (!selectedSchema) return;
    
    const loadSelectedSchema = async () => {
      setIsLoading(true);
      setError(null);
      
      try {
        const [schemaData, yamlContent] = await Promise.all([
          loadSchema(selectedSchema.path),
          loadSchemaRaw(selectedSchema.path)
        ]);
        
        setSchema(schemaData);
        setRawYaml(yamlContent);
      } catch (err) {
        setError(`${t('failedToLoad')} ${selectedSchema.name}`);
        console.error(err);
      } finally {
        setIsLoading(false);
      }
    };
    
    loadSelectedSchema();
  }, [selectedSchema]);

  // Load custodian types from schema annotations when schema changes
  // This pre-loads types asynchronously so they're available for rendering
  // IMPORTANT: Wait for schema service to complete loading before fetching custodian types
  // to avoid race condition where annotations aren't available yet
  useEffect(() => {
    if (!schema) {
      setCustodianTypesLoaded(false);
      return;
    }
    
    // Don't load custodian types until schema service has finished loading all class files
    // This prevents the race condition where we try to read annotations before they're loaded
    if (!isSchemaServiceComplete) {
      console.log('[LinkMLViewerPage] Waiting for schema service to complete before loading custodian types...');
      return;
    }
    
    const loadCustodianTypes = async () => {
      const classes = extractClasses(schema);
      const slots = extractSlots(schema);
      const enums = extractEnums(schema);
      
      console.log('[LinkMLViewerPage] Schema service complete, loading custodian types for', {
        classes: classes.length,
        slots: slots.length,
        enums: enums.length
      });
      
      // Load types for all classes in parallel
      const classTypesPromises = classes.map(async (cls) => {
        const types = await getCustodianTypesForClassAsync(cls.name);
        return [cls.name, types] as const;
      });
      
      // Load types for all slots in parallel
      const slotTypesPromises = slots.map(async (slot) => {
        const types = await getCustodianTypesForSlotAsync(slot.name);
        return [slot.name, types] as const;
      });
      
      // Load types for all enums in parallel
      const enumTypesPromises = enums.map(async (enumDef) => {
        const types = await getCustodianTypesForEnumAsync(enumDef.name);
        return [enumDef.name, types] as const;
      });
      
      try {
        const [classResults, slotResults, enumResults] = await Promise.all([
          Promise.all(classTypesPromises),
          Promise.all(slotTypesPromises),
          Promise.all(enumTypesPromises)
        ]);
        
        // Convert to records
        const classTypesMap: Record<string, CustodianTypeCode[]> = {};
        for (const [name, types] of classResults) {
          classTypesMap[name] = types;
        }
        
        const slotTypesMap: Record<string, CustodianTypeCode[]> = {};
        for (const [name, types] of slotResults) {
          slotTypesMap[name] = types;
        }
        
        const enumTypesMap: Record<string, CustodianTypeCode[]> = {};
        for (const [name, types] of enumResults) {
          enumTypesMap[name] = types;
        }
        
        setClassCustodianTypes(classTypesMap);
        setSlotCustodianTypes(slotTypesMap);
        setEnumCustodianTypes(enumTypesMap);
        setCustodianTypesLoaded(true);
        
        console.log('[LinkMLViewerPage] Loaded custodian types from schema annotations:', {
          classes: Object.keys(classTypesMap).length,
          slots: Object.keys(slotTypesMap).length,
          enums: Object.keys(enumTypesMap).length
        });
      } catch (error) {
        console.error('[LinkMLViewerPage] Error loading custodian types:', error);
        // Fall back to sync functions (will use defaults)
        setCustodianTypesLoaded(true);
      }
    };
    
    loadCustodianTypes();
  }, [schema, isSchemaServiceComplete]);

  // Effect: Pre-load dependency counts for all classes when schema service completes
  // This enables showing import/export counts immediately without waiting for click
  useEffect(() => {
    if (!isSchemaServiceComplete) return;
    
    const loadDependencyCounts = async () => {
      try {
        const counts = await linkmlSchemaService.getAllClassDependencyCounts();
        setDependencyCounts(counts);
        console.log('[LinkMLViewerPage] Pre-loaded dependency counts for', counts.size, 'classes');
      } catch (error) {
        console.error('[LinkMLViewerPage] Error loading dependency counts:', error);
      }
    };
    
    loadDependencyCounts();
  }, [isSchemaServiceComplete]);

  // Effect: Load all slot definitions from schema service for slot lookup
  // This enables resolving slot names to full definitions in class details
  // Also computes semantic labels for UML edge labels
  useEffect(() => {
    if (!isSchemaServiceComplete) return;
    
    const loadServiceSlots = async () => {
      try {
        const slots = await linkmlSchemaService.getAllSlots();
        setServiceSlots(slots);
        
        // Build semantic labels map: slot name -> first exact_mapping or slot_uri
        const labels = new Map<string, string>();
        for (const [name, slot] of slots.entries()) {
          // Priority 1: First exact_mapping (most precise semantic alignment)
          if (slot.exact_mappings && slot.exact_mappings.length > 0) {
            labels.set(name, slot.exact_mappings[0]);
          }
          // Priority 2: slot_uri
          else if (slot.slot_uri) {
            labels.set(name, slot.slot_uri);
          }
          // Priority 3: Fallback to slot name
          else {
            labels.set(name, name);
          }
        }
        setSlotSemanticLabels(labels);
        
        console.log('[LinkMLViewerPage] Pre-loaded slot definitions for', slots.size, 'slots');
        console.log('[LinkMLViewerPage] Computed semantic labels for', labels.size, 'slots');
      } catch (error) {
        console.error('[LinkMLViewerPage] Error loading slot definitions:', error);
      }
    };
    
    loadServiceSlots();
  }, [isSchemaServiceComplete]);

  const toggleSection = (section: string) => {
    setExpandedSections(prev => {
      const next = new Set(prev);
      if (next.has(section)) {
        next.delete(section);
      } else {
        next.add(section);
      }
      return next;
    });
  };

  // Check if a range is an enum type
  const isEnumRange = (range: string): boolean => {
    return range.endsWith('Enum');
  };

  // Toggle enum range expansion and load enum data if needed
  const toggleEnumRange = async (slotName: string, enumName: string) => {
    const key = `${slotName}:${enumName}`;
    
    // Toggle expansion state
    setExpandedEnumRanges(prev => {
      const next = new Set(prev);
      if (next.has(key)) {
        next.delete(key);
      } else {
        next.add(key);
      }
      return next;
    });
    
    // Load enum data if not already loaded
    if (!loadedEnums[enumName]) {
      try {
        const enumSchema = await loadSchema(`modules/enums/${enumName}.yaml`);
        if (enumSchema?.enums) {
          const enumDef = Object.values(enumSchema.enums)[0] as LinkMLEnum;
          if (enumDef) {
            setLoadedEnums(prev => ({ ...prev, [enumName]: { ...enumDef, name: enumName } }));
          }
        }
      } catch (err) {
        console.error(`Failed to load enum ${enumName}:`, err);
        setLoadedEnums(prev => ({ ...prev, [enumName]: null }));
      }
    }
  };

  // Render enum values for a slot range
  const renderEnumValues = (_slotName: string, enumName: string) => {
    const enumDef = loadedEnums[enumName];
    
    if (!enumDef?.permissible_values) {
      return <div className="linkml-viewer__enum-loading">{t('loading')}</div>;
    }
    
    const allValues = Object.entries(enumDef.permissible_values);
    const searchFilter = enumSearchFilters[enumName] || '';
    const showAll = enumShowAll[enumName] || false;
    const displayCount = 20; // Show first 20 values initially
    
    // Filter values based on search
    const filteredValues = searchFilter
      ? allValues.filter(([value, details]) => {
          const searchLower = searchFilter.toLowerCase();
          return (
            value.toLowerCase().includes(searchLower) ||
            (details.description?.toLowerCase().includes(searchLower)) ||
            (details.meaning?.toLowerCase().includes(searchLower))
          );
        })
      : allValues;
    
    // Determine how many to show
    const valuesToShow = showAll || searchFilter ? filteredValues : filteredValues.slice(0, displayCount);
    
    return (
      <div className="linkml-viewer__range-enum-values">
        <div className="linkml-viewer__range-enum-header">
          {/* Search input */}
          <div className="linkml-viewer__range-enum-search">
            <input
              type="text"
              placeholder={t('searchPlaceholder')}
              value={searchFilter}
              onChange={(e) => setEnumSearchFilters(prev => ({ ...prev, [enumName]: e.target.value }))}
              className="linkml-viewer__range-enum-search-input"
            />
            {searchFilter && (
              <button
                className="linkml-viewer__range-enum-search-clear"
                onClick={() => setEnumSearchFilters(prev => ({ ...prev, [enumName]: '' }))}
                title="Clear search"
              >
                ×
              </button>
            )}
          </div>
          
          {/* Count display */}
          <span className="linkml-viewer__range-enum-count">
            {searchFilter ? (
              <>
                {t('showing')} {filteredValues.length} {t('of')} {allValues.length}
              </>
            ) : showAll ? (
              <>
                {allValues.length} {t('permissibleValues').replace(':', '')}
              </>
            ) : (
              <>
                {t('showing')} {Math.min(displayCount, allValues.length)} {t('of')} {allValues.length}
              </>
            )}
          </span>
        </div>
        
        <div className="linkml-viewer__range-enum-list">
          {valuesToShow.length > 0 ? (
            valuesToShow.map(([value, details]) => (
              <div key={value} className="linkml-viewer__range-enum-item">
                <code className="linkml-viewer__range-enum-name">{value}</code>
                {details.meaning && (
                  <a 
                    href={details.meaning.startsWith('wikidata:')
                      ? `https://www.wikidata.org/wiki/${details.meaning.replace('wikidata:', '')}`
                      : details.meaning}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="linkml-viewer__range-enum-link"
                    title={details.meaning}
                  >
                    🔗
                  </a>
                )}
                {details.description && (
                  <span className="linkml-viewer__range-enum-desc">{details.description}</span>
                )}
              </div>
            ))
          ) : (
            <div className="linkml-viewer__range-enum-no-results">
              {t('noResults')}
            </div>
          )}
        </div>
        
        {/* Show all / Show less button */}
        {!searchFilter && allValues.length > displayCount && (
          <button
            className="linkml-viewer__range-enum-toggle-all"
            onClick={() => setEnumShowAll(prev => ({ ...prev, [enumName]: !showAll }))}
          >
            {showAll ? (
              <>▲ {t('showLess')}</>
            ) : (
              <>▼ {t('showAll')} ({allValues.length - displayCount} {language === 'nl' ? 'meer' : 'more'})</>
            )}
          </button>
        )}
      </div>
    );
  };

  const renderClassDetails = (cls: LinkMLClass) => {
    const isHighlighted = highlightedClass === cls.name;
    // Use pre-loaded types from schema annotations, fall back to sync function if not yet loaded
    const custodianTypes = classCustodianTypes[cls.name] || getCustodianTypesForClass(cls.name);
    const isUniversal = isUniversalElement(custodianTypes);
    
    // Check if this class matches the current custodian type filter (multi-select)
    const matchesFilter = custodianTypeFilter.size === 0 || custodianTypes.some(t => custodianTypeFilter.has(t));
    
    return (
      <div 
        key={cls.name} 
        ref={isHighlighted ? highlightedRef : null}
        className={`linkml-viewer__item ${isHighlighted ? 'linkml-viewer__item--highlighted' : ''} ${!matchesFilter ? 'linkml-viewer__item--filtered-out' : ''}`}
      >
        <h4 className="linkml-viewer__item-name">
          {cls.name}
          {cls.abstract && <span className="linkml-viewer__badge linkml-viewer__badge--abstract">{t('abstract')}</span>}
          {/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */}
          {use3DIndicator ? (
            <CustodianTypeIndicator3D 
              types={custodianTypes} 
              size={32}
              animate={true}
              showTooltip={true}
              hoveredType={hoveredCustodianType}
              onTypeHover={setHoveredCustodianType}
              // Removed onFaceClick - cube should be decorative, not trigger filtering
              className="linkml-viewer__custodian-indicator"
            />
          ) : (
            !isUniversal && (
              <CustodianTypeBadge 
                types={custodianTypes} 
                size="small" 
                className="linkml-viewer__custodian-badge"
              />
            )
          )}
        </h4>
        {cls.class_uri && (
          <div className="linkml-viewer__uri">
            <span className="linkml-viewer__label">{t('uri')}</span>
            <code>{cls.class_uri}</code>
          </div>
        )}
        {cls.description && (
          <div className="linkml-viewer__description linkml-viewer__markdown">
            <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(cls.description)}</ReactMarkdown>
          </div>
        )}
        {/* Class Slots Section - Accordion showing slot details for this class */}
        {cls.slots && cls.slots.length > 0 && (
          <div className="linkml-viewer__class-slots-section">
            <button 
              className={`linkml-viewer__class-slots-toggle ${expandedClassSlots.has(cls.name) ? 'linkml-viewer__class-slots-toggle--expanded' : ''}`}
              onClick={() => toggleClassSlots(cls.name)}
              title={t('classSlotsTooltip')}
            >
              <span className="linkml-viewer__class-slots-icon">{expandedClassSlots.has(cls.name) ? '▼' : '▶'}</span>
              <span className="linkml-viewer__label">{t('classSlots')}</span>
              <span className="linkml-viewer__class-slots-count">{cls.slots.length}</span>
            </button>
            {expandedClassSlots.has(cls.name) && (
              <div className="linkml-viewer__class-slots-content">
                {cls.slots.map(slotName => {
                  // Get generic slot definition from the global slot lookup
                  const genericSlot = slotLookupMap.get(slotName);
                  // Get class-specific slot_usage overrides (if any)
                  const slotUsage = cls.slot_usage?.[slotName];
                  
                  // Merge generic slot with slot_usage overrides
                  // slot_usage properties take precedence over generic slot properties
                  if (genericSlot || slotUsage) {
                    const mergedSlot: LinkMLSlot = {
                      name: slotName,
                      ...(genericSlot || {}),
                      ...(slotUsage || {}),
                    };
                    return renderSlotDetails(mergedSlot, slotUsage || null, genericSlot || null, cls.name);
                  }
                  // Fallback: show slot name as tag if neither generic definition nor slot_usage found
                  return (
                    <div key={slotName} className="linkml-viewer__item linkml-viewer__item--slot-fallback">
                      <h4 className="linkml-viewer__item-name">{slotName}</h4>
                      <div className="linkml-viewer__description">
                        <em>Slot definition not found in schema</em>
                      </div>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
        )}
        {cls.exact_mappings && cls.exact_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label">{t('exactMappings')}</span>
          <div className="linkml-viewer__tag-list">
            {cls.exact_mappings.map(mapping => (
              <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping">{mapping}</span>
            ))}
          </div>
        </div>
      )}
        {cls.close_mappings && cls.close_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label">{t('closeMappings')}</span>
            <div className="linkml-viewer__tag-list">
              {cls.close_mappings.map(mapping => (
                <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--close">{mapping}</span>
              ))}
            </div>
          </div>
        )}
        {/* Imports Section - Shows forward dependencies (what this class depends on) */}
        {isSchemaServiceComplete && (
          <div className="linkml-viewer__imports-section">
            <button 
              className={`linkml-viewer__imports-toggle ${expandedImports.has(cls.name) ? 'linkml-viewer__imports-toggle--expanded' : ''}`}
              onClick={() => toggleImports(cls.name)}
              title={t('classImportsTooltip')}
            >
              <span className="linkml-viewer__imports-icon">{expandedImports.has(cls.name) ? '▼' : '▶'}</span>
              <span className="linkml-viewer__label">{t('classImports')}</span>
              {/* Show pre-loaded count OR detailed count if data loaded */}
              {classImports[cls.name] ? (
                <span className="linkml-viewer__imports-count">
                  {(classImports[cls.name].parentClass ? 1 : 0) + 
                   classImports[cls.name].mixins.length + 
                   classImports[cls.name].slotRanges.length + 
                   classImports[cls.name].slotUsageRanges.length}
                </span>
              ) : dependencyCounts.get(cls.name) ? (
                <span className="linkml-viewer__imports-count">
                  {dependencyCounts.get(cls.name)!.importCount}
                </span>
              ) : null}
              {loadingImports.has(cls.name) && <span className="linkml-viewer__imports-loading">Loading...</span>}
            </button>
            {expandedImports.has(cls.name) && classImports[cls.name] && (
              <div className="linkml-viewer__imports-content">
                {/* Parent Class (is_a) */}
                {classImports[cls.name].parentClass && (
                  <div className="linkml-viewer__imports-category">
                    <span className="linkml-viewer__imports-category-label">{t('parentClassLabel')}</span>
                    <div className="linkml-viewer__imports-list">
                      <button 
                        className="linkml-viewer__imports-link"
                        onClick={() => navigateToClass(classImports[cls.name].parentClass!)}
                      >
                        {classImports[cls.name].parentClass}
                      </button>
                    </div>
                  </div>
                )}
                {/* Mixins */}
                {classImports[cls.name].mixins.length > 0 && (
                  <div className="linkml-viewer__imports-category">
                    <span className="linkml-viewer__imports-category-label">{t('mixinsLabel')} ({classImports[cls.name].mixins.length})</span>
                    <div className="linkml-viewer__imports-list">
                      {classImports[cls.name].mixins.map(mixin => (
                        <button 
                          key={mixin} 
                          className="linkml-viewer__imports-link"
                          onClick={() => navigateToClass(mixin)}
                        >
                          {mixin}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* Slot Ranges */}
                {classImports[cls.name].slotRanges.length > 0 && (
                  <div className="linkml-viewer__imports-category">
                    <span className="linkml-viewer__imports-category-label">{t('slotRangesLabel')} ({classImports[cls.name].slotRanges.length})</span>
                    <div className="linkml-viewer__imports-list">
                      {classImports[cls.name].slotRanges.map(({ slotName, rangeType, isClass }) => (
                        <button 
                          key={`${slotName}-${rangeType}`} 
                          className="linkml-viewer__imports-link"
                          onClick={() => navigateToClass(rangeType)}
                          title={`slot: ${slotName}`}
                        >
                          {rangeType} <span className="linkml-viewer__imports-via">via {slotName}</span>
                          {!isClass && <span className="linkml-viewer__imports-badge linkml-viewer__imports-badge--enum">enum</span>}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* Slot Usage Ranges */}
                {classImports[cls.name].slotUsageRanges.length > 0 && (
                  <div className="linkml-viewer__imports-category">
                    <span className="linkml-viewer__imports-category-label">{t('slotUsageRangesLabel')} ({classImports[cls.name].slotUsageRanges.length})</span>
                    <div className="linkml-viewer__imports-list">
                      {classImports[cls.name].slotUsageRanges.map(({ slotName, rangeType }) => (
                        <button 
                          key={`usage-${slotName}-${rangeType}`} 
                          className="linkml-viewer__imports-link"
                          onClick={() => navigateToClass(rangeType)}
                          title={`slot_usage: ${slotName}`}
                        >
                          {rangeType} <span className="linkml-viewer__imports-via">via {slotName}</span>
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* No imports message */}
                {!classImports[cls.name].parentClass &&
                 classImports[cls.name].mixins.length === 0 &&
                 classImports[cls.name].slotRanges.length === 0 &&
                 classImports[cls.name].slotUsageRanges.length === 0 && (
                  <div className="linkml-viewer__imports-empty">
                    {t('noImports')}
                  </div>
                )}
              </div>
            )}
          </div>
        )}
        {/* Exports Section - Shows reverse dependencies (what references this class) */}
        {isSchemaServiceComplete && (
          <div className="linkml-viewer__exports-section">
            <button 
              className={`linkml-viewer__exports-toggle ${expandedExports.has(cls.name) ? 'linkml-viewer__exports-toggle--expanded' : ''}`}
              onClick={() => toggleExports(cls.name)}
            >
              <span className="linkml-viewer__exports-icon">{expandedExports.has(cls.name) ? '▼' : '▶'}</span>
              <span className="linkml-viewer__label">Exports</span>
              {/* Show pre-loaded count OR detailed count if data loaded */}
              {classExports[cls.name] ? (
                <span className="linkml-viewer__exports-count">
                  {classExports[cls.name].subclasses.length + 
                   classExports[cls.name].mixinUsers.length + 
                   classExports[cls.name].classesUsingSlotWithThisRange.length + 
                   classExports[cls.name].classesReferencingInSlotUsage.length}
                </span>
              ) : dependencyCounts.get(cls.name) ? (
                <span className="linkml-viewer__exports-count">
                  {dependencyCounts.get(cls.name)!.exportCount}
                </span>
              ) : null}
              {loadingExports.has(cls.name) && <span className="linkml-viewer__exports-loading">Loading...</span>}
            </button>
            {expandedExports.has(cls.name) && classExports[cls.name] && (
              <div className="linkml-viewer__exports-content">
                {/* Subclasses */}
                {classExports[cls.name].subclasses.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Subclasses ({classExports[cls.name].subclasses.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].subclasses.map(subclass => (
                        <button 
                          key={subclass} 
                          className="linkml-viewer__exports-link"
                          onClick={() => navigateToClass(subclass)}
                        >
                          {subclass}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* Mixin Users */}
                {classExports[cls.name].mixinUsers.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Used as Mixin by ({classExports[cls.name].mixinUsers.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].mixinUsers.map(user => (
                        <button 
                          key={user} 
                          className="linkml-viewer__exports-link"
                          onClick={() => navigateToClass(user)}
                        >
                          {user}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* Slots with this Range */}
                {classExports[cls.name].slotsWithThisRange.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Slots with this Range ({classExports[cls.name].slotsWithThisRange.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].slotsWithThisRange.map(slot => (
                        <span key={slot} className="linkml-viewer__exports-item linkml-viewer__exports-item--slot">
                          {slot}
                        </span>
                      ))}
                    </div>
                  </div>
                )}
                {/* Classes using slots with this range */}
                {classExports[cls.name].classesUsingSlotWithThisRange.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Classes Using Slots with this Range ({classExports[cls.name].classesUsingSlotWithThisRange.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].classesUsingSlotWithThisRange.map(({ className, slotName }) => (
                        <button 
                          key={`${className}-${slotName}`} 
                          className="linkml-viewer__exports-link"
                          onClick={() => navigateToClass(className)}
                          title={`via slot: ${slotName}`}
                        >
                          {className} <span className="linkml-viewer__exports-via">via {slotName}</span>
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* Classes referencing in slot_usage */}
                {classExports[cls.name].classesReferencingInSlotUsage.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Referenced in slot_usage ({classExports[cls.name].classesReferencingInSlotUsage.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].classesReferencingInSlotUsage.map(refClass => (
                        <button 
                          key={refClass} 
                          className="linkml-viewer__exports-link"
                          onClick={() => navigateToClass(refClass)}
                        >
                          {refClass}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* No exports message */}
                {classExports[cls.name].subclasses.length === 0 &&
                 classExports[cls.name].mixinUsers.length === 0 &&
                 classExports[cls.name].slotsWithThisRange.length === 0 &&
                 classExports[cls.name].classesUsingSlotWithThisRange.length === 0 &&
                 classExports[cls.name].classesReferencingInSlotUsage.length === 0 && (
                  <div className="linkml-viewer__exports-empty">
                    No other schema elements reference this class.
                  </div>
                )}
              </div>
            )}
          </div>
        )}
        
        {/* UML Diagram Section - Shows filtered class relationship diagram */}
        {isSchemaServiceComplete && (
          <div className="linkml-viewer__uml-section">
            <button 
              className={`linkml-viewer__uml-toggle ${expandedUML.has(cls.name) ? 'linkml-viewer__uml-toggle--expanded' : ''}`}
              onClick={() => toggleUML(cls.name)}
            >
              <span className="linkml-viewer__uml-icon">{expandedUML.has(cls.name) ? '▼' : '▶'}</span>
              <span className="linkml-viewer__label">UML Diagram</span>
              {(loadingExports.has(cls.name) || loadingImports.has(cls.name)) && <span className="linkml-viewer__uml-loading">Loading...</span>}
            </button>
            {/* Show UML content when expanded AND required data is loaded */}
            {/* Must wait for BOTH exports AND imports since imports contain slot range info needed for aggregation links */}
            {expandedUML.has(cls.name) && classExports[cls.name] && classImports[cls.name] && (
              <div className="linkml-viewer__uml-content">
                {/* Direction toggles (Imports/Exports checkboxes) */}
                <div className="linkml-viewer__uml-direction-control">
                  <label className="linkml-viewer__uml-checkbox-label linkml-viewer__uml-checkbox-label--imports" title={t('umlImportsTooltip')}>
                    <input
                      type="checkbox"
                      checked={umlShowImports[cls.name] !== false}
                      onChange={(e) => setUmlShowImports(prev => ({ ...prev, [cls.name]: e.target.checked }))}
                      className="linkml-viewer__uml-checkbox linkml-viewer__uml-checkbox--imports"
                    />
                    <span>{t('umlShowImports')}</span>
                  </label>
                  <label className="linkml-viewer__uml-checkbox-label linkml-viewer__uml-checkbox-label--exports" title={t('umlExportsTooltip')}>
                    <input
                      type="checkbox"
                      checked={umlShowExports[cls.name] !== false}
                      onChange={(e) => setUmlShowExports(prev => ({ ...prev, [cls.name]: e.target.checked }))}
                      className="linkml-viewer__uml-checkbox linkml-viewer__uml-checkbox--exports"
                    />
                    <span>{t('umlShowExports')}</span>
                  </label>
                </div>
                {/* Depth slider */}
                <div className="linkml-viewer__uml-depth-control">
                  <label className="linkml-viewer__uml-depth-label">
                    <span>Relationship Depth:</span>
                    <input
                      type="range"
                      min="1"
                      max="4"
                      value={umlDepth[cls.name] || 1}
                      onChange={(e) => {
                        const newDepth = parseInt(e.target.value, 10);
                        setUmlDepth(prev => ({ ...prev, [cls.name]: newDepth }));
                        // Use debounced loading to prevent excessive API calls while dragging
                        if (newDepth > 1) {
                          debouncedLoadRelationshipInfo(cls.name, newDepth);
                        }
                      }}
                      className="linkml-viewer__uml-depth-slider"
                    />
                    <span className="linkml-viewer__uml-depth-value">{umlDepth[cls.name] || 1}</span>
                  </label>
                  <span className="linkml-viewer__uml-depth-hint">
                    {(umlDepth[cls.name] || 1) === 1 ? 'Direct relationships' : 
                     (umlDepth[cls.name] || 1) === 2 ? 'Secondary connections' :
                     (umlDepth[cls.name] || 1) === 3 ? 'Tertiary connections' : 'Extended network'}
                  </span>
                </div>
                {/* UML Toolbar */}
                <div className="linkml-viewer__uml-toolbar">
                  {/* Zoom Controls */}
                  <button 
                    className="linkml-viewer__uml-toolbar-btn"
                    title="Fit to screen"
                    onClick={() => window.dispatchEvent(new CustomEvent('uml-fit-to-screen'))}
                  >
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
                      <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
                    </svg>
                    <span>Fit</span>
                  </button>
                  <button 
                    className="linkml-viewer__uml-toolbar-btn"
                    title="Zoom in"
                    onClick={() => window.dispatchEvent(new CustomEvent('uml-zoom-in'))}
                  >
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
                      <circle cx="11" cy="11" r="8" />
                      <path d="m21 21-4.35-4.35" />
                      <line x1="11" y1="8" x2="11" y2="14" />
                      <line x1="8" y1="11" x2="14" y2="11" />
                    </svg>
                  </button>
                  <button 
                    className="linkml-viewer__uml-toolbar-btn"
                    title="Zoom out"
                    onClick={() => window.dispatchEvent(new CustomEvent('uml-zoom-out'))}
                  >
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
                      <circle cx="11" cy="11" r="8" />
                      <path d="m21 21-4.35-4.35" />
                      <line x1="8" y1="11" x2="14" y2="11" />
                    </svg>
                  </button>
                  <button 
                    className="linkml-viewer__uml-toolbar-btn"
                    title="Reset view"
                    onClick={() => window.dispatchEvent(new CustomEvent('uml-reset-view'))}
                  >
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
                      <polyline points="1 4 1 10 7 10" />
                      <polyline points="23 20 23 14 17 14" />
                      <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" />
                    </svg>
                  </button>
                  
                  <div className="linkml-viewer__uml-toolbar-separator" />
                  
                  {/* Fullscreen Toggle */}
                  <button 
                    className={`linkml-viewer__uml-toolbar-btn ${umlFullscreen === cls.name ? 'linkml-viewer__uml-toolbar-btn--active' : ''}`}
                    title={umlFullscreen === cls.name ? 'Exit fullscreen' : 'Fullscreen'}
                    onClick={() => setUmlFullscreen(umlFullscreen === cls.name ? null : cls.name)}
                  >
                    {umlFullscreen === cls.name ? (
                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
                        <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" />
                      </svg>
                    ) : (
                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
                        <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
                      </svg>
                    )}
                  </button>
                  
                  <div className="linkml-viewer__uml-toolbar-separator" />
                  
                  {/* Layout Dropdown */}
                  <div className="linkml-viewer__uml-layout-dropdown">
                    <button 
                      className="linkml-viewer__uml-toolbar-btn"
                      onClick={() => setUmlLayoutDropdownOpen(prev => ({ ...prev, [cls.name]: !prev[cls.name] }))}
                      title={`Layout: ${umlLayoutType[cls.name] || 'dagre'} ${umlLayoutType[cls.name] === 'dagre' ? `(${umlDagreDirection[cls.name] || 'TB'})` : ''}`}
                    >
                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="16" height="16">
                        <rect x="3" y="3" width="7" height="7" />
                        <rect x="14" y="3" width="7" height="7" />
                        <rect x="14" y="14" width="7" height="7" />
                        <rect x="3" y="14" width="7" height="7" />
                      </svg>
                      <span>Layout</span>
                      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="12" height="12">
                        <path d="m6 9 6 6 6-6" />
                      </svg>
                    </button>
                    
                    {umlLayoutDropdownOpen[cls.name] && (
                      <div className="linkml-viewer__uml-layout-menu">
                        <button 
                          className={`linkml-viewer__uml-layout-option ${(umlLayoutType[cls.name] || 'dagre') === 'dagre' && (umlDagreDirection[cls.name] || 'TB') === 'TB' ? 'linkml-viewer__uml-layout-option--active' : ''}`}
                          onClick={() => {
                            setUmlLayoutType(prev => ({ ...prev, [cls.name]: 'dagre' }));
                            setUmlDagreDirection(prev => ({ ...prev, [cls.name]: 'TB' }));
                            setUmlLayoutDropdownOpen(prev => ({ ...prev, [cls.name]: false }));
                          }}
                        >
                          <span className="linkml-viewer__uml-layout-title">Hierarchical (Top-Bottom)</span>
                          <span className="linkml-viewer__uml-layout-desc">Tree structure flowing downward</span>
                        </button>
                        <button 
                          className={`linkml-viewer__uml-layout-option ${(umlLayoutType[cls.name] || 'dagre') === 'dagre' && umlDagreDirection[cls.name] === 'LR' ? 'linkml-viewer__uml-layout-option--active' : ''}`}
                          onClick={() => {
                            setUmlLayoutType(prev => ({ ...prev, [cls.name]: 'dagre' }));
                            setUmlDagreDirection(prev => ({ ...prev, [cls.name]: 'LR' }));
                            setUmlLayoutDropdownOpen(prev => ({ ...prev, [cls.name]: false }));
                          }}
                        >
                          <span className="linkml-viewer__uml-layout-title">Hierarchical (Left-Right)</span>
                          <span className="linkml-viewer__uml-layout-desc">Tree structure flowing rightward</span>
                        </button>
                        <button 
                          className={`linkml-viewer__uml-layout-option ${umlLayoutType[cls.name] === 'force' ? 'linkml-viewer__uml-layout-option--active' : ''}`}
                          onClick={() => {
                            setUmlLayoutType(prev => ({ ...prev, [cls.name]: 'force' }));
                            setUmlLayoutDropdownOpen(prev => ({ ...prev, [cls.name]: false }));
                          }}
                        >
                          <span className="linkml-viewer__uml-layout-title">Force-Directed</span>
                          <span className="linkml-viewer__uml-layout-desc">Physics-based node positioning</span>
                        </button>
                        <button 
                          className={`linkml-viewer__uml-layout-option ${umlLayoutType[cls.name] === 'circular' ? 'linkml-viewer__uml-layout-option--active' : ''}`}
                          onClick={() => {
                            setUmlLayoutType(prev => ({ ...prev, [cls.name]: 'circular' }));
                            setUmlLayoutDropdownOpen(prev => ({ ...prev, [cls.name]: false }));
                          }}
                        >
                          <span className="linkml-viewer__uml-layout-title">Circular</span>
                          <span className="linkml-viewer__uml-layout-desc">Nodes arranged in a circle</span>
                        </button>
                        <button 
                          className={`linkml-viewer__uml-layout-option ${umlLayoutType[cls.name] === 'radial' ? 'linkml-viewer__uml-layout-option--active' : ''}`}
                          onClick={() => {
                            setUmlLayoutType(prev => ({ ...prev, [cls.name]: 'radial' }));
                            setUmlLayoutDropdownOpen(prev => ({ ...prev, [cls.name]: false }));
                          }}
                        >
                          <span className="linkml-viewer__uml-layout-title">Radial</span>
                          <span className="linkml-viewer__uml-layout-desc">Center-outward tree layout</span>
                        </button>
                      </div>
                    )}
                  </div>
                </div>
                
                {/* Show empty state when both toggles are unchecked */}
                {umlShowImports[cls.name] === false && umlShowExports[cls.name] === false ? (
                  <div className="linkml-viewer__uml-empty-state">
                    <span className="linkml-viewer__uml-empty-icon">🔗</span>
                    <p>{t('umlNoRelationshipsShown')}</p>
                  </div>
                ) : (
                  <div 
                    className={`linkml-viewer__uml-container ${umlFullscreen === cls.name ? 'linkml-viewer__uml-container--fullscreen' : ''}`}
                    style={umlFullscreen !== cls.name ? { 
                      width: '100%', 
                      height: '720px' 
                    } : undefined}
                  >
                    <UMLVisualization
                      diagram={getMemoizedUMLDiagram(
                        cls.name, 
                        umlDepth[cls.name] || 1,
                        umlShowImports[cls.name] !== false,
                        umlShowExports[cls.name] !== false
                      )}
                      width={umlFullscreen === cls.name ? window.innerWidth : undefined}
                      height={umlFullscreen === cls.name ? window.innerHeight : 720}
                      layoutType={umlLayoutType[cls.name] || 'dagre'}
                      dagreDirection={umlDagreDirection[cls.name] || 'TB'}
                    />
                    {umlFullscreen === cls.name && (
                      <button 
                        className="linkml-viewer__uml-fullscreen-close"
                        onClick={() => setUmlFullscreen(null)}
                        title="Exit fullscreen (Esc)"
                      >
                        ✕
                      </button>
                    )}
                  </div>
                )}
              </div>
            )}
            {/* Show message when expanded but data not yet loaded */}
            {expandedUML.has(cls.name) && (!classExports[cls.name] || !classImports[cls.name]) && !loadingExports.has(cls.name) && !loadingImports.has(cls.name) && (
              <div className="linkml-viewer__uml-empty">
                Click to load class relationships.
              </div>
            )}
          </div>
        )}
      </div>
    );
  };

  const renderSlotDetails = (
    slot: LinkMLSlot, 
    slotUsage: Partial<LinkMLSlot> | null = null, 
    genericSlot: LinkMLSlot | null = null,
    className: string | null = null
  ) => {
    const hasSlotUsageOverrides = slotUsage !== null;
    const rangeIsEnum = slot.range && isEnumRange(slot.range);
    const enumKey = slot.range ? `${slot.name}:${slot.range}` : '';
    const isExpanded = expandedEnumRanges.has(enumKey);
    // Use pre-loaded types from schema annotations, fall back to sync function if not yet loaded
    const custodianTypes = slotCustodianTypes[slot.name] || getCustodianTypesForSlot(slot.name);
    const isUniversal = isUniversalElement(custodianTypes);
    
    // Check if this slot matches the current custodian type filter (multi-select)
    const matchesFilter = custodianTypeFilter.size === 0 || custodianTypes.some(t => custodianTypeFilter.has(t));
    
    // Check if comparison view is expanded for this slot
    const comparisonKey = className ? `${className}:${slot.name}` : slot.name;
    const showComparison = expandedSlotComparisons.has(comparisonKey);
    
    // Helper to check if a property is overridden in slot_usage
    const isOverridden = (property: keyof LinkMLSlot): boolean => {
      if (!slotUsage) return false;
      return slotUsage[property] !== undefined;
    };
    
    // Helper to render override marker
    const OverrideMarker = ({ property }: { property: keyof LinkMLSlot }) => {
      if (!hasSlotUsageOverrides) return null;
      const overridden = isOverridden(property);
      return (
        <span 
          className={`linkml-viewer__override-marker ${overridden ? 'linkml-viewer__override-marker--overridden' : 'linkml-viewer__override-marker--inherited'}`}
          title={overridden ? t('overriddenInSlotUsage') : t('inheritedFromGeneric')}
        >
          {overridden ? t('slotUsageOverrideMarker') : ''}
        </span>
      );
    };
    
    return (
      <div key={slot.name} className={`linkml-viewer__item ${!matchesFilter ? 'linkml-viewer__item--filtered-out' : ''} ${hasSlotUsageOverrides ? 'linkml-viewer__item--has-usage' : ''}`}>
        <h4 className="linkml-viewer__item-name">
          {slot.name}
          {hasSlotUsageOverrides && <span className="linkml-viewer__badge linkml-viewer__badge--usage" title={t('slotUsageTooltip')}>{t('slotUsageBadge')}</span>}
          {/* Compare button - only show when slot_usage exists and we have both generic and override */}
          {hasSlotUsageOverrides && genericSlot && className && (
            <button 
              className={`linkml-viewer__badge linkml-viewer__badge--compare ${showComparison ? 'linkml-viewer__badge--compare-active' : ''}`}
              onClick={(e) => {
                e.stopPropagation();
                toggleSlotComparison(className, slot.name);
              }}
              title={t('slotUsageCompareTooltip')}
            >
              {showComparison ? '▼' : '▶'} {t('slotUsageCompareToggle')}
            </button>
          )}
          {slot.required && <span className="linkml-viewer__badge linkml-viewer__badge--required">{t('required')}</span>}
          {slot.multivalued && <span className="linkml-viewer__badge linkml-viewer__badge--multi">{t('multivalued')}</span>}
          {slot.identifier && <span className="linkml-viewer__badge linkml-viewer__badge--identifier">{t('identifier')}</span>}
          {/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */}
          {use3DIndicator ? (
            <CustodianTypeIndicator3D 
              types={custodianTypes} 
              size={32}
              animate={true}
              showTooltip={true}
              hoveredType={hoveredCustodianType}
              onTypeHover={setHoveredCustodianType}
              // Removed onFaceClick - cube should be decorative, not trigger filtering
              className="linkml-viewer__custodian-indicator"
            />
          ) : (
            !isUniversal && (
              <CustodianTypeBadge 
                types={custodianTypes} 
                size="small" 
                className="linkml-viewer__custodian-badge"
              />
            )
          )}
        </h4>
        {/* Slot URI - semantic predicate */}
        {slot.slot_uri && (
          <div className="linkml-viewer__slot-uri">
            <span className="linkml-viewer__label"><OverrideMarker property="slot_uri" />{t('slotUri')}</span>
            <code className="linkml-viewer__uri-value">{slot.slot_uri}</code>
          </div>
        )}
        {slot.range && (
          <div className="linkml-viewer__range">
            <span className="linkml-viewer__label"><OverrideMarker property="range" />{t('range')}</span>
            {rangeIsEnum ? (
              <button 
                className={`linkml-viewer__range-enum-toggle ${isExpanded ? 'linkml-viewer__range-enum-toggle--expanded' : ''}`}
                onClick={() => toggleEnumRange(slot.name, slot.range!)}
              >
                <span className="linkml-viewer__range-enum-icon">{isExpanded ? '▼' : '▶'}</span>
                <code>{slot.range}</code>
                <span className="linkml-viewer__badge linkml-viewer__badge--enum">enum</span>
              </button>
            ) : (
              <code>{slot.range}</code>
            )}
          </div>
        )}
        {/* Expandable enum values */}
        {rangeIsEnum && isExpanded && slot.range && renderEnumValues(slot.name, slot.range)}
        {slot.description && (
          <div className="linkml-viewer__description linkml-viewer__markdown">
            <span className="linkml-viewer__label linkml-viewer__label--inline"><OverrideMarker property="description" /></span>
            <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(slot.description)}</ReactMarkdown>
          </div>
        )}
        {slot.pattern && (
          <div className="linkml-viewer__pattern">
            <span className="linkml-viewer__label"><OverrideMarker property="pattern" />{t('pattern')}</span>
            <code>{slot.pattern}</code>
          </div>
        )}
        {/* Examples section */}
        {slot.examples && slot.examples.length > 0 && (
          <div className="linkml-viewer__examples">
            <span className="linkml-viewer__label"><OverrideMarker property="examples" />{t('examples')}</span>
            <ul className="linkml-viewer__examples-list">
              {slot.examples.map((example, idx) => (
                <li key={idx} className="linkml-viewer__example-item">
                  <code className="linkml-viewer__example-value">{example.value}</code>
                  {example.description && (
                    <span className="linkml-viewer__example-description"> - {example.description}</span>
                  )}
                </li>
              ))}
            </ul>
          </div>
        )}
        {/* Semantic Mappings - 5 types per LinkML spec */}
        {slot.exact_mappings && slot.exact_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label"><OverrideMarker property="exact_mappings" />{t('exactMappings')}</span>
            <div className="linkml-viewer__tag-list">
              {slot.exact_mappings.map(mapping => (
                <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--exact">{mapping}</span>
              ))}
            </div>
          </div>
        )}
        {slot.close_mappings && slot.close_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label"><OverrideMarker property="close_mappings" />{t('closeMappings')}</span>
            <div className="linkml-viewer__tag-list">
              {slot.close_mappings.map(mapping => (
                <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--close">{mapping}</span>
              ))}
            </div>
          </div>
        )}
        {slot.narrow_mappings && slot.narrow_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label"><OverrideMarker property="narrow_mappings" />{t('narrowMappings')}</span>
            <div className="linkml-viewer__tag-list">
              {slot.narrow_mappings.map(mapping => (
                <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--narrow">{mapping}</span>
              ))}
            </div>
          </div>
        )}
        {slot.broad_mappings && slot.broad_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label"><OverrideMarker property="broad_mappings" />{t('broadMappings')}</span>
            <div className="linkml-viewer__tag-list">
              {slot.broad_mappings.map(mapping => (
                <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--broad">{mapping}</span>
              ))}
            </div>
          </div>
        )}
        {slot.related_mappings && slot.related_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label"><OverrideMarker property="related_mappings" />{t('relatedMappings')}</span>
            <div className="linkml-viewer__tag-list">
              {slot.related_mappings.map(mapping => (
                <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--related">{mapping}</span>
              ))}
            </div>
          </div>
        )}
        {/* Comments section */}
        {slot.comments && slot.comments.length > 0 && (
          <div className="linkml-viewer__comments">
            <span className="linkml-viewer__label"><OverrideMarker property="comments" />{t('comments')}</span>
            <ul className="linkml-viewer__comments-list">
              {slot.comments.map((comment, idx) => (
                <li key={idx} className="linkml-viewer__comment-item">{comment}</li>
              ))}
            </ul>
          </div>
        )}
        
        {/* Side-by-side comparison view - only shown when expanded */}
        {showComparison && hasSlotUsageOverrides && genericSlot && (
          <div className="linkml-viewer__slot-comparison">
            <div className="linkml-viewer__slot-comparison-header">
              <h5>{t('slotUsageCompareGeneric')}</h5>
              <h5>{t('slotUsageCompareOverride')}</h5>
            </div>
            <div className="linkml-viewer__slot-comparison-content">
              {/* Compare key properties side by side */}
              {renderComparisonRow('range', genericSlot.range, slotUsage?.range)}
              {renderComparisonRow('description', genericSlot.description, slotUsage?.description)}
              {renderComparisonRow('required', genericSlot.required, slotUsage?.required)}
              {renderComparisonRow('multivalued', genericSlot.multivalued, slotUsage?.multivalued)}
              {renderComparisonRow('slot_uri', genericSlot.slot_uri, slotUsage?.slot_uri)}
              {renderComparisonRow('pattern', genericSlot.pattern, slotUsage?.pattern)}
              {renderComparisonRow('identifier', genericSlot.identifier, slotUsage?.identifier)}
            </div>
          </div>
        )}
      </div>
    );
  };
  
  // Helper function to render a comparison row
  const renderComparisonRow = (
    property: string, 
    genericValue: string | boolean | number | undefined | null, 
    overrideValue: string | boolean | number | undefined | null
  ) => {
    const hasGeneric = genericValue !== undefined && genericValue !== null;
    const hasOverride = overrideValue !== undefined && overrideValue !== null;
    const isChanged = hasOverride && hasGeneric && genericValue !== overrideValue;
    const isNewInOverride = hasOverride && !hasGeneric;
    
    // Don't show row if neither has a value
    if (!hasGeneric && !hasOverride) return null;
    
    const formatValue = (val: string | boolean | number | undefined | null): string => {
      if (val === undefined || val === null) return '';
      if (typeof val === 'boolean') return val ? 'true' : 'false';
      if (typeof val === 'number') return String(val);
      // Truncate long strings for display
      const str = String(val);
      return str.length > 80 ? str.substring(0, 77) + '...' : str;
    };
    
    return (
      <div key={property} className={`linkml-viewer__slot-comparison-row ${isChanged || isNewInOverride ? 'linkml-viewer__slot-comparison-row--changed' : ''}`}>
        <div className="linkml-viewer__slot-comparison-label">{property}</div>
        <div className="linkml-viewer__slot-comparison-generic">
          {hasGeneric ? (
            <code title={typeof genericValue === 'string' ? genericValue : undefined}>{formatValue(genericValue)}</code>
          ) : (
            <span className="linkml-viewer__slot-comparison-empty">{t('slotUsageCompareNotDefined')}</span>
          )}
        </div>
        <div className={`linkml-viewer__slot-comparison-override ${isChanged || isNewInOverride ? 'linkml-viewer__slot-comparison-override--diff' : ''}`}>
          {hasOverride ? (
            <>
              <code title={typeof overrideValue === 'string' ? overrideValue : undefined}>{formatValue(overrideValue)}</code>
              {(isChanged || isNewInOverride) && <span className="linkml-viewer__slot-comparison-diff-badge">{t('slotUsageCompareDiff')}</span>}
            </>
          ) : (
            <span className="linkml-viewer__slot-comparison-inherited">{t('slotUsageCompareInherited')}</span>
          )}
        </div>
      </div>
    );
  };

  const renderEnumDetails = (enumDef: LinkMLEnum) => {
    const enumName = enumDef.name;
    const allValues = enumDef.permissible_values ? Object.entries(enumDef.permissible_values) : [];
    const searchFilter = enumSearchFilters[enumName] || '';
    const showAll = enumShowAll[enumName] || false;
    const displayCount = 20;
    const custodianTypes = enumCustodianTypes[enumDef.name] || getCustodianTypesForEnum(enumDef.name);
    const isUniversal = isUniversalElement(custodianTypes);
    
    // Filter values based on search
    const filteredValues = searchFilter
      ? allValues.filter(([value, details]) => {
          const searchLower = searchFilter.toLowerCase();
          return (
            value.toLowerCase().includes(searchLower) ||
            (details.description?.toLowerCase().includes(searchLower)) ||
            (details.meaning?.toLowerCase().includes(searchLower))
          );
        })
      : allValues;
    
    // Determine how many to show
    const valuesToShow = showAll || searchFilter ? filteredValues : filteredValues.slice(0, displayCount);
    
    // Check if this enum matches the current custodian type filter (multi-select)
    const matchesFilter = custodianTypeFilter.size === 0 || custodianTypes.some(t => custodianTypeFilter.has(t));
    
    return (
      <div key={enumDef.name} className={`linkml-viewer__item ${!matchesFilter ? 'linkml-viewer__item--filtered-out' : ''}`}>
        <h4 className="linkml-viewer__item-name">
          {enumDef.name}
          {/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */}
          {use3DIndicator ? (
            <CustodianTypeIndicator3D 
              types={custodianTypes} 
              size={32}
              animate={true}
              showTooltip={true}
              hoveredType={hoveredCustodianType}
              onTypeHover={setHoveredCustodianType}
              // Removed onFaceClick - cube should be decorative, not trigger filtering
              className="linkml-viewer__custodian-indicator"
            />
          ) : (
            !isUniversal && (
              <CustodianTypeBadge 
                types={custodianTypes} 
                size="small" 
                className="linkml-viewer__custodian-badge"
              />
            )
          )}
        </h4>
        {enumDef.description && (
          <div className="linkml-viewer__description linkml-viewer__markdown">
            <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(enumDef.description)}</ReactMarkdown>
          </div>
        )}
        {allValues.length > 0 && (
          <div className="linkml-viewer__enum-values">
            {/* Search input */}
            <div className="linkml-viewer__range-enum-search">
              <input
                type="text"
                placeholder={t('searchPlaceholder')}
                value={searchFilter}
                onChange={(e) => setEnumSearchFilters(prev => ({ ...prev, [enumName]: e.target.value }))}
                className="linkml-viewer__range-enum-search-input"
              />
              {searchFilter && (
                <button
                  className="linkml-viewer__range-enum-search-clear"
                  onClick={() => setEnumSearchFilters(prev => ({ ...prev, [enumName]: '' }))}
                  title="Clear search"
                >
                  ×
                </button>
              )}
            </div>
            
            {/* Count display */}
            <span className="linkml-viewer__range-enum-count" style={{ display: 'block', marginBottom: '0.5rem' }}>
              {searchFilter ? (
                <>
                  {t('showing')} {filteredValues.length} {t('of')} {allValues.length}
                </>
              ) : showAll ? (
                <>
                  {allValues.length} {t('permissibleValues').replace(':', '')}
                </>
              ) : (
                <>
                  {t('showing')} {Math.min(displayCount, allValues.length)} {t('of')} {allValues.length}
                </>
              )}
            </span>
            
            <div className="linkml-viewer__value-list">
              {valuesToShow.length > 0 ? (
                valuesToShow.map(([value, details]) => (
                  <div key={value} className="linkml-viewer__value-item">
                    <code className="linkml-viewer__value-name">{value}</code>
                    {details.description && (
                      <div className="linkml-viewer__value-desc linkml-viewer__markdown linkml-viewer__markdown--compact">
                        <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(details.description)}</ReactMarkdown>
                      </div>
                    )}
                    {details.meaning && (
                      <span className="linkml-viewer__value-meaning">
                        <span className="linkml-viewer__label">{t('meaning')}</span> {details.meaning}
                      </span>
                    )}
                  </div>
                ))
              ) : (
                <div className="linkml-viewer__range-enum-no-results">
                  {t('noResults')}
                </div>
              )}
            </div>
            
            {/* Show all / Show less button */}
            {!searchFilter && allValues.length > displayCount && (
              <button
                className="linkml-viewer__range-enum-toggle-all"
                onClick={() => setEnumShowAll(prev => ({ ...prev, [enumName]: !showAll }))}
              >
                {showAll ? (
                  <>▲ {t('showLess')}</>
                ) : (
                  <>▼ {t('showAll')} ({allValues.length - displayCount} {language === 'nl' ? 'meer' : 'more'})</>
                )}
              </button>
            )}
          </div>
        )}
      </div>
    );
  };

  const renderVisualView = () => {
    if (!schema) return null;
    
    const classes = extractClasses(schema);
    const enums = extractEnums(schema);
    const slots = extractSlots(schema);
    
    // Count matching items when filter is active (for display purposes)
    const matchingClassCount = custodianTypeFilter.size > 0
      ? classes.filter(cls => {
          const types = classCustodianTypes[cls.name] || getCustodianTypesForClass(cls.name);
          return types.some(t => custodianTypeFilter.has(t));
        }).length
      : classes.length;
    
    const matchingEnumCount = custodianTypeFilter.size > 0
      ? enums.filter(enumDef => {
          const types = enumCustodianTypes[enumDef.name] || getCustodianTypesForEnum(enumDef.name);
          return types.some(t => custodianTypeFilter.has(t));
        }).length
      : enums.length;
    
    const matchingSlotCount = custodianTypeFilter.size > 0
      ? slots.filter(slot => {
          const types = slotCustodianTypes[slot.name] || getCustodianTypesForSlot(slot.name);
          return types.some(t => custodianTypeFilter.has(t));
        }).length
      : slots.length;
    
    return (
      <div className="linkml-viewer__visual">
        {/* Schema Metadata */}
        <div className="linkml-viewer__metadata">
          <h2 className="linkml-viewer__schema-name">{formatDisplayName(schema.name || schema.title || '') || t('unnamedSchema')}</h2>
          {schema.description && (
            <div className="linkml-viewer__schema-desc linkml-viewer__markdown">
              <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(schema.description)}</ReactMarkdown>
            </div>
          )}
          {schema.id && (
            <div className="linkml-viewer__schema-id">
              <span className="linkml-viewer__label">{t('id')}</span>
              <code>{schema.id}</code>
            </div>
          )}
          {schema.version && (
            <div className="linkml-viewer__schema-version">
              <span className="linkml-viewer__label">{t('version')}</span>
              <code>{schema.version}</code>
            </div>
          )}
        </div>

        {/* Prefixes */}
        {schema.prefixes && Object.keys(schema.prefixes).length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('prefixes')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('prefixes') ? '▼' : '▶'}
              </span>
              {t('prefixes')} ({Object.keys(schema.prefixes).length})
            </button>
            {expandedSections.has('prefixes') && (
              <div className="linkml-viewer__prefix-list">
                {Object.entries(schema.prefixes).map(([prefix, uri]) => (
                  <div key={prefix} className="linkml-viewer__prefix-item">
                    <code className="linkml-viewer__prefix-name">{prefix}:</code>
                    <span className="linkml-viewer__prefix-uri">{uri}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}

        {/* Classes */}
        {classes.length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('classes')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('classes') ? '▼' : '▶'}
              </span>
              {t('classes')} ({custodianTypeFilter.size > 0 ? `${matchingClassCount}/${classes.length}` : classes.length})
            </button>
            {expandedSections.has('classes') && (
              <div className="linkml-viewer__section-content">
                {classes.map(renderClassDetails)}
              </div>
            )}
          </div>
        )}

        {/* Slots */}
        {slots.length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('slots')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('slots') ? '▼' : '▶'}
              </span>
              {t('slots')} ({custodianTypeFilter.size > 0 ? `${matchingSlotCount}/${slots.length}` : slots.length})
            </button>
            {expandedSections.has('slots') && (
              <div className="linkml-viewer__section-content">
                {slots.map(slot => renderSlotDetails(slot))}
              </div>
            )}
          </div>
        )}

        {/* Enums */}
        {enums.length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('enums')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('enums') ? '▼' : '▶'}
              </span>
              {t('enumerations')} ({custodianTypeFilter.size > 0 ? `${matchingEnumCount}/${enums.length}` : enums.length})
            </button>
            {expandedSections.has('enums') && (
              <div className="linkml-viewer__section-content">
                {enums.map(renderEnumDetails)}
              </div>
            )}
          </div>
        )}

        {/* Imports - File Level (LinkML module imports) */}
        {schema.imports && schema.imports.length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('imports')}
              title={t('fileImportsTooltip')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('imports') ? '▼' : '▶'}
              </span>
              {t('imports')} ({schema.imports.length})
              <span 
                className="linkml-viewer__info-icon" 
                data-tooltip={t('fileImportsTooltip')}
                onClick={(e) => e.stopPropagation()}
              >ⓘ</span>
            </button>
            {expandedSections.has('imports') && (
              <div className="linkml-viewer__import-list">
                {schema.imports.map(imp => (
                  <div key={imp} className="linkml-viewer__import-item">
                    <code>{imp}</code>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    );
  };

  const renderRawView = () => {
    if (!rawYaml) return null;
    
    const handleCopyYaml = async () => {
      try {
        await navigator.clipboard.writeText(rawYaml);
        setCopyFeedback(true);
        setTimeout(() => setCopyFeedback(false), 2000);
      } catch (err) {
        console.error('Failed to copy YAML:', err);
      }
    };
    
    return (
      <div className="linkml-viewer__raw">
        <div className="linkml-viewer__raw-header">
          <button
            className={`linkml-viewer__copy-btn ${copyFeedback ? 'linkml-viewer__copy-btn--copied' : ''}`}
            onClick={handleCopyYaml}
            title={copyFeedback ? t('copied') : t('copyToClipboard')}
          >
            {copyFeedback ? '✓' : '⧉'} {copyFeedback ? t('copied') : t('copyToClipboard')}
          </button>
        </div>
        <pre className="linkml-viewer__yaml">{rawYaml}</pre>
      </div>
    );
  };

  return (
    <div className={`linkml-viewer-page ${sidebarCollapsed ? 'linkml-viewer-page--sidebar-collapsed' : ''}`}>
      {/* Left Sidebar - Schema Files */}
      <aside className={`linkml-viewer-page__sidebar ${sidebarCollapsed ? 'linkml-viewer-page__sidebar--collapsed' : ''}`}>
        <div className="linkml-viewer-page__sidebar-header">
          <h2 className="linkml-viewer-page__sidebar-title">{!sidebarCollapsed && t('sidebarTitle')}</h2>
          <button
            className="linkml-viewer-page__sidebar-toggle"
            onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
            title={sidebarCollapsed ? t('expandSidebar') : t('collapseSidebar')}
            aria-label={sidebarCollapsed ? t('expandSidebar') : t('collapseSidebar')}
          >
            {sidebarCollapsed ? '»' : '«'}
          </button>
        </div>
        
        {!sidebarCollapsed && (
          <>
        
        {/* Sidebar Search */}
        <div className="linkml-viewer-page__sidebar-search">
          <input
            type="text"
            placeholder={t('searchSchemas')}
            value={sidebarSearch}
            onChange={(e) => setSidebarSearch(e.target.value)}
            className="linkml-viewer-page__sidebar-search-input"
          />
          {sidebarSearch && (
            <button
              className="linkml-viewer-page__sidebar-search-clear"
              onClick={() => setSidebarSearch('')}
              title="Clear search"
            >
              ×
            </button>
          )}
        </div>
        
        {/* Category Filters */}
        <div className="linkml-viewer-page__sidebar-filters">
          <label className="linkml-viewer-page__filter-checkbox">
            <input
              type="checkbox"
              checked={categoryFilters.main}
              onChange={(e) => setCategoryFilters(prev => ({ ...prev, main: e.target.checked }))}
            />
            <span className="linkml-viewer-page__filter-label linkml-viewer-page__filter-label--main">
              {t('mainSchema')}
            </span>
          </label>
          <label className="linkml-viewer-page__filter-checkbox">
            <input
              type="checkbox"
              checked={categoryFilters.class}
              onChange={(e) => setCategoryFilters(prev => ({ ...prev, class: e.target.checked }))}
            />
            <span className="linkml-viewer-page__filter-label linkml-viewer-page__filter-label--class">
              {t('classes')}
            </span>
          </label>
          <label className="linkml-viewer-page__filter-checkbox">
            <input
              type="checkbox"
              checked={categoryFilters.enum}
              onChange={(e) => setCategoryFilters(prev => ({ ...prev, enum: e.target.checked }))}
            />
            <span className="linkml-viewer-page__filter-label linkml-viewer-page__filter-label--enum">
              {t('enumerations')}
            </span>
          </label>
          <label className="linkml-viewer-page__filter-checkbox">
            <input
              type="checkbox"
              checked={categoryFilters.slot}
              onChange={(e) => setCategoryFilters(prev => ({ ...prev, slot: e.target.checked }))}
            />
            <span className="linkml-viewer-page__filter-label linkml-viewer-page__filter-label--slot">
              {t('slots')}
            </span>
          </label>
        </div>
        
        {categories
          .filter(category => categoryFilters[category.name] !== false)
          .map(category => {
            // Filter files based on search
            const filteredFiles = sidebarSearch
              ? category.files.filter(file => 
                  file.name.toLowerCase().includes(sidebarSearch.toLowerCase())
                )
              : category.files;
            
            // Don't render empty categories
            if (filteredFiles.length === 0) return null;
            
            return (
              <div key={category.name} className="linkml-viewer-page__category">
                <h3 className="linkml-viewer-page__category-title">
                  {category.displayName}
                  <span className="linkml-viewer-page__category-count">
                    ({filteredFiles.length}{sidebarSearch ? `/${category.files.length}` : ''})
                  </span>
                </h3>
                <div className="linkml-viewer-page__file-list">
                  {filteredFiles.map(file => (
                    <button
                      key={file.path}
                      className={`linkml-viewer-page__file-item ${
                        selectedSchema?.path === file.path ? 'linkml-viewer-page__file-item--active' : ''
                      }`}
                      onClick={() => setSelectedSchema(file)}
                    >
                      {file.name}
                    </button>
                  ))}
                </div>
              </div>
            );
          })}
        
        {/* No results message */}
        {sidebarSearch && categories.every(category => 
          !categoryFilters[category.name] || 
          category.files.filter(file => 
            file.name.toLowerCase().includes(sidebarSearch.toLowerCase())
          ).length === 0
        ) && (
          <div className="linkml-viewer-page__no-results">
            {t('noMatchingSchemas')}
          </div>
        )}
          </>
        )}
      </aside>

      {/* Main Content */}
      <main className="linkml-viewer-page__main" ref={mainContentRef}>
        {/* Collapsible header with title - hides when navigation collapses */}
        <header className="linkml-viewer-page__header">
          <h1 className="linkml-viewer-page__title">
            {selectedSchema ? formatDisplayName(selectedSchema.name) : t('pageTitle')}
          </h1>
        </header>
        
        {/* Sticky subheader with tabs - always visible */}
        <div className="linkml-viewer-page__subheader">
          <div className="linkml-viewer-page__tabs">
            <button
              className={`linkml-viewer-page__tab ${viewMode === 'visual' ? 'linkml-viewer-page__tab--active' : ''}`}
              onClick={() => setViewMode('visual')}
            >
              {t('visualView')}
            </button>
            <button
              className={`linkml-viewer-page__tab ${viewMode === 'raw' ? 'linkml-viewer-page__tab--active' : ''}`}
              onClick={() => setViewMode('raw')}
            >
              {t('rawYaml')}
            </button>
            <span className="linkml-viewer-page__tab-separator">|</span>
            <button
              className={`linkml-viewer-page__tab linkml-viewer-page__tab--indicator ${use3DIndicator ? 'linkml-viewer-page__tab--active' : ''}`}
              onClick={() => setUse3DIndicator(!use3DIndicator)}
              title={use3DIndicator ? t('use2DBadge') : t('use3DPolygon')}
            >
              {use3DIndicator ? '🔷 3D' : '🏷️ 2D'}
            </button>
            <span className="linkml-viewer-page__tab-separator">|</span>
            <button
              className={`linkml-viewer-page__tab linkml-viewer-page__tab--legend ${showSlotUsageLegend ? 'linkml-viewer-page__tab--active' : ''}`}
              onClick={() => setShowSlotUsageLegend(!showSlotUsageLegend)}
              title={t('slotUsageLegendTitle')}
            >
              ❓ {t('slotUsageLegendToggle')}
            </button>
          </div>
          
          {/* Slot Usage Legend - explains the ✦ symbol and green indicators */}
          {showSlotUsageLegend && (
            <div className="linkml-viewer-page__slot-usage-legend">
              <div className="linkml-viewer-page__slot-usage-legend-header">
                <h4>{t('slotUsageLegendTitle')}</h4>
                <button 
                  className="linkml-viewer-page__slot-usage-legend-close"
                  onClick={() => setShowSlotUsageLegend(false)}
                  title="Close"
                >×</button>
              </div>
              <div className="linkml-viewer-page__slot-usage-legend-content">
                <div className="linkml-viewer-page__slot-usage-legend-item">
                  <span className="linkml-viewer__badge linkml-viewer__badge--usage">slot_usage</span>
                  <span>{t('slotUsageLegendBadge')}</span>
                </div>
                <div className="linkml-viewer-page__slot-usage-legend-item">
                  <span className="linkml-viewer-page__slot-usage-legend-marker">✦</span>
                  <span>{t('slotUsageLegendMarker')}</span>
                </div>
                <div className="linkml-viewer-page__slot-usage-legend-item">
                  <span className="linkml-viewer-page__slot-usage-legend-border-demo"></span>
                  <span>{t('slotUsageLegendBorder')}</span>
                </div>
                <div className="linkml-viewer-page__slot-usage-legend-item linkml-viewer-page__slot-usage-legend-item--info">
                  <span>ℹ️</span>
                  <span>{t('slotUsageLegendInherited')}</span>
                </div>
              </div>
            </div>
          )}
          
          {/* Legend bar for 3D mode - shows all 19 custodian types with bidirectional hover sync */}
          {use3DIndicator && (
            <div className="linkml-viewer-page__legend-bar">
              <CustodianTypeLegendBar
                hoveredType={hoveredCustodianType}
                onTypeHover={setHoveredCustodianType}
                onTypeClick={handleCustodianTypeFilter}
                highlightTypes={Array.from(custodianTypeFilter)}
                size="small"
              />
              {custodianTypeFilter.size > 0 && (
                <button
                  className="linkml-viewer-page__filter-clear"
                  onClick={() => setCustodianTypeFilter(new Set())}
                  title={language === 'nl' ? 'Filter wissen' : 'Clear filter'}
                >
                  ✕ {language === 'nl' ? 'Filter wissen' : 'Clear filter'} ({Array.from(custodianTypeFilter).join(', ')})
                </button>
              )}
            </div>
          )}
        </div>

        <div className="linkml-viewer-page__content">
          {isLoading || isSchemaServiceLoading ? (
            <LoadingScreen
              message={schemaProgress?.message || t('loading')}
              progress={schemaProgress?.percent}
              size="medium"
              fullscreen={false}
            />
          ) : error ? (
            <div className="linkml-viewer-page__error">{error}</div>
          ) : viewMode === 'visual' ? (
            renderVisualView()
          ) : (
            renderRawView()
          )}
        </div>
      </main>
    </div>
  );
};

export default LinkMLViewerPage;
