• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
01Critter Models 02
└ Locust└ Butterfly└ Cockroach└ Spider└ Scorpion└ Dragon└ Brazilian Treehopper└ Tardigrade└ Diatoms└ Bumblebee└ Wasp└ Hammerhead Worm└ Giraffe Weevil└ Trilobite Beetle
02VantaJS Shaders
└ Clouds└ Waves└ Cells└ Rings└ Halo└ Globe└ Net└ Dots└ Birds└ Trunk└ Topology└ About VantaJS
03App Bundle 02
└ Gutindex└ Insect Builder└ OpenLayers
04Cathedrals
└ Lattice└ Lost└ Lucent└ Lumen└ Glass└ Disco└ Hyperion└ Latent
05App Bundle 01
└ Robohash└ PrismJS└ Horoscope└ Binary Jazz└ Morse Code└ URL Extractor└ Entity Escaper└ Phonetic Alphabet└ Toki Pona└ Semaphore└ Cut Up Machine└ Text Mixer└ Currency Freaks└ Host IO└ CrUX API└ Logo Dev└ Web Speech API└ GEO DB
06Retroscopic
└ Galaxy└ Ocean└ Slime└ 24-Cell└ Unicorn└ Polyp└ Oneiric 1└ Oneiric 2└ Node└ Torus Knot└ GLSL Grid└ Liquid Crystal└ Silky└ Butterfly└ Ocean 2└ Icosahedron└ Luma└ Swarm└ Broken Solid└ Hypersphere└ Constraints└ Psycho Killer
01 / 03

GUTINDEX - INTERACTIVE APP SOURCE CODE

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gutendex Explorer</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link
        href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Inter:wght@400;500;600&display=swap"
        rel="stylesheet">
    <style>
        :root {
            /* Map to Binaural Light mode defaults */
            --bg-black: #ffffff;
            --bg-darker: #f5f3f3;
            --bg-dark: #f5f3f3;
            --bg-card: transparent;
            --border-color: rgba(0, 0, 0, 0.18);
            --text-white: #000000;
            --text-light: #5d5f5f;
            --text-muted: #5d5f5f;
            --accent-primary: #000000;
            --accent-secondary: #5d5f5f;
            --accent-success: #00e676;
            --accent-warning: #ff9100;
            --accent-danger: #ff1744;
            --radius-sm: 0px;
            --radius-md: 0px;
        }

        @media (prefers-color-scheme: dark) {
            :root {
                --bg-black: #050505;
                --bg-darker: #141515;
                --bg-dark: #141515;
                --bg-card: transparent;
                --border-color: rgba(255, 255, 255, 0.18);
                --text-white: #ffffff;
                --text-light: #848484;
                --text-muted: #848484;
                --accent-primary: #ffffff;
                --accent-secondary: #848484;
            }
        }
        
        [data-theme="dark"] {
            --bg-black: #050505;
            --bg-darker: #141515;
            --bg-dark: #141515;
            --bg-card: transparent;
            --border-color: rgba(255, 255, 255, 0.18);
            --text-white: #ffffff;
            --text-light: #848484;
            --text-muted: #848484;
            --accent-primary: #ffffff;
            --accent-secondary: #848484;
            --modal-overlay: rgba(0, 0, 0, 0.95);
            --reader-overlay: rgba(0, 0, 0, 0.98);
            --reader-bg: #000000;
        }

        [data-theme="light"] {
            --bg-black: #ffffff;
            --bg-darker: #f5f3f3;
            --bg-dark: #f5f3f3;
            --bg-card: transparent;
            --border-color: rgba(0, 0, 0, 0.18);
            --text-white: #000000;
            --text-light: #5d5f5f;
            --text-muted: #5d5f5f;
            --accent-primary: #000000;
            --accent-secondary: #5d5f5f;
            --modal-overlay: rgba(255, 255, 255, 0.95);
            --reader-overlay: rgba(255, 255, 255, 0.98);
            --reader-bg: #ffffff;
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Inter', sans-serif;
            background-color: var(--bg-black);
            color: var(--text-white);
            line-height: 1.4;
            font-size: 13px;
            overflow-x: hidden;
            height: 100vh;
            display: flex;
        }

        .mono {
            font-family: 'JetBrains Mono', monospace;
        }

        /* Main Layout */
        .main-layout {
            display: flex;
            width: 100%;
            height: 100vh;
            overflow: hidden;
        }

        /* Left Panel - Filters */
        .left-panel {
            width: 320px;
            background-color: var(--bg-dark);
            border-right: 1px solid var(--border-color);
            padding: 20px;
            overflow-y: auto;
            height: 100vh;
        }

        .panel-section {
            margin-bottom: 24px;
        }

        .panel-title {
            font-family: 'JetBrains Mono', monospace;
            font-size: 11px;
            font-weight: 700;
            letter-spacing: 0.15em;
            text-transform: uppercase;
            color: var(--text-muted);
            margin-bottom: 16px;
            padding-bottom: 10px;
            border-bottom: 1px solid var(--border-color);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .panel-title i {
            display: none;
        }

        /* Filter Controls */
        .filter-group {
            margin-bottom: 16px;
        }

        .filter-label {
            display: block;
            font-size: 11px;
            color: var(--text-light);
            margin-bottom: 6px;
            font-weight: 500;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .filter-label .help {
            color: var(--text-muted);
            font-size: 9px;
            cursor: help;
        }

        .filter-input,
        .filter-select {
            width: 100%;
            padding: 8px 10px;
            background: var(--bg-dark);
            border: 1px solid var(--border-color);
            border-radius: 0;
            color: var(--text-white);
            font-size: 12px;
            font-family: 'JetBrains Mono', monospace;
            transition: border-color 0.1s;
            outline: none;
        }

        .filter-input:focus,
        .filter-select:focus {
            border-color: var(--text-white);
        }

        .filter-checkbox {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-bottom: 6px;
            cursor: pointer;
            font-size: 12px;
            color: var(--text-light);
        }

        .filter-checkbox input[type="checkbox"] {
            accent-color: var(--accent-primary);
            width: 14px;
            height: 14px;
        }

        .filter-actions {
            display: flex;
            gap: 10px;
            margin-top: 20px;
        }

        .btn {
            font-family: 'JetBrains Mono', monospace;
            font-size: 11px;
            font-weight: 700;
            letter-spacing: 0.15em;
            text-transform: uppercase;
            background: transparent;
            border: 1px solid var(--border-color);
            color: var(--text-white);
            padding: 10px 16px;
            cursor: pointer;
            transition: all 0.15s ease;
            border-radius: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
            flex: 1;
            text-align: center;
        }

        .btn:hover {
            background: var(--bg-dark);
            border-color: var(--text-white);
        }

        .btn-secondary {
            background-color: transparent;
            border: 1px solid var(--border-color);
            color: var(--text-light);
        }

        .btn-secondary:hover {
            background-color: rgba(255, 255, 255, 0.05);
            border-color: var(--accent-primary);
        }

        /* Main Content Area */
        .main-content {
            flex: 1;
            padding: 20px;
            overflow-y: auto;
            height: 100vh;
        }

        /* Search Controls */
        .search-controls {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 16px;
            margin-bottom: 20px;
        }

        .search-panel {
            background: transparent;
            border: 1px solid var(--border-color);
            border-radius: 0;
            padding: 20px;
        }

        .search-panel-title {
            font-family: 'JetBrains Mono', monospace;
            font-size: 11px;
            font-weight: 700;
            letter-spacing: 0.15em;
            text-transform: uppercase;
            color: var(--text-muted);
            margin-bottom: 16px;
            padding-bottom: 10px;
            border-bottom: 1px solid var(--border-color);
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .search-panel-title i {
            display: none;
        }

        .search-input-group {
            display: flex;
            gap: 8px;
        }

        .search-input {
            flex: 1;
            padding: 10px 12px;
            background: var(--bg-dark);
            border: 1px solid var(--border-color);
            border-radius: 0;
            color: var(--text-white);
            font-size: 12px;
            font-family: 'JetBrains Mono', monospace;
            transition: border-color 0.1s;
            outline: none;
        }

        .search-input:focus {
            border-color: var(--text-white);
        }

        .search-examples {
            margin-top: 10px;
            font-size: 10px;
            color: var(--text-muted);
            line-height: 1.5;
        }

        .search-examples span {
            color: var(--accent-secondary);
            cursor: pointer;
            text-decoration: underline;
            margin-right: 8px;
        }

        .search-examples span:hover {
            color: var(--text-light);
        }

        .search-execute {
            display: flex;
            justify-content: flex-end;
            margin-top: 16px;
        }

        .search-execute .btn {
            width: auto;
            min-width: 140px;
        }

        /* Stats Bar */
        .stats-container {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 12px;
            margin-bottom: 20px;
        }

        .stat-card {
            background: transparent;
            border: 1px solid var(--border-color);
            border-radius: 0;
            padding: 12px;
            text-align: center;
        }

        .stat-value {
            font-size: 18px;
            font-weight: 600;
            color: var(--accent-primary);
            font-family: 'JetBrains Mono', monospace;
            margin-bottom: 4px;
        }

        .stat-label {
            font-size: 10px;
            color: var(--text-muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        /* Books Grid */
        .books-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 16px;
            margin-bottom: 24px;
        }

        .book-card {
            background: transparent;
            border: 1px solid var(--border-color);
            border-radius: 0;
            overflow: hidden;
            transition: border-color 0.1s, background-color 0.1s;
            cursor: pointer;
            position: relative;
        }

        .book-card:hover {
            border-color: var(--text-white);
        }

        .book-header {
            padding: 12px;
            border-bottom: 1px solid var(--border-color);
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
        }

        .book-id {
            font-family: 'JetBrains Mono', monospace;
            font-size: 11px;
            font-weight: 700;
            color: var(--text-white);
            background: var(--bg-dark);
            padding: 3px 8px;
            border: 1px solid var(--border-color);
            border-radius: 0;
            min-width: 60px;
            text-align: right;
        }

        .book-title {
            font-size: 13px;
            font-weight: 600;
            line-height: 1.4;
            color: var(--text-white);
            margin-bottom: 6px;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
            overflow: hidden;
        }

        .book-author {
            font-size: 11px;
            color: var(--accent-secondary);
            margin-bottom: 8px;
        }

        .book-body {
            padding: 12px;
        }

        .book-meta {
            display: flex;
            justify-content: space-between;
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid var(--border-color);
        }

        .meta-item {
            text-align: center;
        }

        .meta-value {
            font-size: 12px;
            font-weight: 600;
            color: var(--text-white);
            font-family: 'JetBrains Mono', monospace;
        }

        .meta-label {
            font-size: 9px;
            color: var(--text-muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
            margin-top: 2px;
        }

        /* Pagination */
        .pagination {
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 16px;
            margin-top: 20px;
        }

        .page-info {
            font-size: 12px;
            color: var(--text-muted);
            font-family: 'JetBrains Mono', monospace;
        }

        .btn-icon {
            padding: 6px 10px;
            background-color: transparent;
            border: 1px solid var(--border-color);
            color: var(--text-light);
            border-radius: var(--radius-sm);
            cursor: pointer;
            transition: all 0.15s;
        }

        .btn-icon:hover:not(:disabled) {
            background-color: rgba(61, 90, 254, 0.1);
            border-color: var(--accent-primary);
        }

        .btn-icon:disabled {
            opacity: 0.4;
            cursor: not-allowed;
        }

        /* Torus Knot Loader */
        .torus-loader {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            padding: 80px 40px;
            width: 100%;
            grid-column: 1 / -1;
            min-height: 50vh;
            transition: opacity .4s ease;
        }
        .torus-loader.is-out { opacity: 0; }
        .torus-loader canvas { width: 100px; height: 100px; display: block; margin: 0 auto; }
        .torus-loader .loading-text {
            margin-top: 20px;
            color: var(--text-muted);
            font-size: 13px;
            font-weight: 500;
            letter-spacing: 2px;
            animation: pulse 1.5s infinite;
        }
        @keyframes pulse {
            0% { opacity: 0.5; }
            50% { opacity: 1; }
            100% { opacity: 0.5; }
        }

        /* Modal */
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: var(--modal-overlay);
            z-index: 1000;
            padding: 20px;
            overflow-y: auto;
        }

        .modal-content {
            background-color: var(--bg-card);
            border-radius: var(--radius-md);
            border: 1px solid var(--border-color);
            max-width: 800px;
            margin: 0 auto;
            position: relative;
            max-height: 90vh;
            overflow-y: auto;
        }

        .modal-header {
            padding: 16px;
            border-bottom: 1px solid var(--border-color);
            display: flex;
            justify-content: space-between;
            align-items: center;
            position: sticky;
            top: 0;
            background-color: var(--bg-card);
            z-index: 10;
        }

        .modal-title {
            font-size: 16px;
            font-weight: 600;
            color: var(--text-white);
            flex: 1;
        }

        .close-modal {
            background: none;
            border: none;
            color: var(--text-muted);
            font-size: 18px;
            cursor: pointer;
            padding: 0;
            width: 24px;
            height: 24px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-left: 16px;
        }

        .close-modal:hover {
            color: var(--text-white);
        }

        .modal-body {
            padding: 16px;
            font-size: 12px;
        }

        .modal-section {
            margin-bottom: 20px;
        }

        .modal-section-title {
            font-size: 13px;
            font-weight: 600;
            color: var(--accent-primary);
            margin-bottom: 8px;
            display: flex;
            align-items: center;
            gap: 6px;
        }

        .modal-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 16px;
            margin-bottom: 16px;
        }

        .modal-item {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }

        .modal-label {
            font-size: 10px;
            color: var(--text-muted);
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }

        .modal-value {
            color: var(--text-light);
            font-size: 12px;
        }

        .tags-container {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
            margin-top: 4px;
        }

        .tag {
            background-color: var(--bg-black);
            color: var(--text-light);
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 10px;
            border: 1px solid var(--border-color);
        }

        .format-tag {
            background-color: rgba(61, 90, 254, 0.1);
            color: var(--accent-primary);
            border-color: var(--accent-primary);
            text-decoration: none;
            transition: all 0.15s;
        }

        .format-tag:hover {
            background-color: rgba(61, 90, 254, 0.2);
        }

        /* DARK MODE READER BUTTONS */
        .dark-reader-btn, .text-reader-btn {
            font-family: 'JetBrains Mono', monospace;
            font-size: 11px;
            font-weight: 700;
            letter-spacing: 0.15em;
            text-transform: uppercase;
            background: transparent;
            color: var(--text-white);
            border: 1px solid var(--border-color);
            padding: 10px 16px;
            border-radius: 0;
            cursor: pointer;
            transition: all 0.15s;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            text-decoration: none;
            margin-top: 8px;
        }

        .text-reader-btn:hover {
            background: var(--bg-dark);
            border-color: var(--text-white);
        }

        /* Format Buttons Container */
        .format-buttons {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            margin-top: 16px;
        }

        /* Reader Modal */
        .reader-modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: var(--reader-overlay);
            z-index: 2000;
            padding: 0;
            overflow: hidden;
        }

        .reader-content {
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            background-color: var(--reader-bg);
        }

        .reader-header {
            padding: 16px;
            border-bottom: 1px solid var(--border-color);
            display: flex;
            justify-content: space-between;
            align-items: center;
            background-color: var(--bg-dark);
        }

        .reader-title {
            font-size: 14px;
            font-weight: 600;
            color: var(--text-white);
            flex: 1;
        }

        .reader-controls {
            display: flex;
            gap: 10px;
            align-items: center;
        }

        .reader-body {
            flex: 1;
            padding: 30px;
            overflow-y: auto;
            background-color: var(--reader-bg);
            color: var(--text-light);
            font-family: 'Georgia', serif;
            line-height: 1.8;
            font-size: 16px;
        }

        .reader-body h1,
        .reader-body h2,
        .reader-body h3 {
            color: var(--text-white);
            margin-bottom: 20px;
            font-family: 'Inter', sans-serif;
        }

        .reader-body p {
            margin-bottom: 20px;
            text-align: justify;
        }

        .reader-body pre,
        .reader-body code {
            background-color: var(--bg-dark);
            padding: 10px;
            border-radius: 4px;
            font-family: 'JetBrains Mono', monospace;
            font-size: 14px;
            overflow-x: auto;
        }

        /* Active Search Terms */
        #active-searches {
            margin-bottom: 20px;
            display: none;
        }

        #search-tags {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        /* Responsive */
        @media (max-width: 1200px) {
            .main-layout {
                flex-direction: column;
                height: auto;
                overflow-y: auto;
            }

            .left-panel {
                width: 100%;
                height: auto;
                border-right: none;
                border-bottom: 1px solid var(--border-color);
            }

            .main-content {
                height: auto;
                overflow-y: visible;
            }

            .search-controls {
                grid-template-columns: 1fr;
            }
        }

        @media (max-width: 768px) {
            .stats-container {
                grid-template-columns: repeat(2, 1fr);
            }

            .books-grid {
                grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            }

            .modal-grid {
                grid-template-columns: 1fr;
            }

            .modal-content {
                max-height: 85vh;
            }

            .reader-body {
                padding: 20px;
                font-size: 14px;
            }
        }

        /* Custom Scrollbar */
        ::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }

        ::-webkit-scrollbar-track {
            background: var(--bg-dark);
        }

        ::-webkit-scrollbar-thumb {
            background: var(--border-color);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: var(--text-muted);
        }
    </style>
</head>

<body>
    <!-- Main Layout -->
    <div class="main-layout">
        <!-- Left Panel - Filters -->
        <aside class="left-panel">
            <div class="panel-section">
                <div class="panel-title">
                    <i class="fas fa-filter"></i> QUERY FILTERS
                </div>

                <div class="filter-group">
                    <label class="filter-label">
                        LANGUAGE FILTER
                        <span class="help" title="Filter by book language">?</span>
                    </label>
                    <select id="language-select" class="filter-select">
                        <option value="">ALL LANGUAGES</option>
                        <option value="en">ENGLISH</option>
                        <option value="fr">FRENCH</option>
                        <option value="de">GERMAN</option>
                        <option value="es">SPANISH</option>
                        <option value="it">ITALIAN</option>
                        <option value="pt">PORTUGUESE</option>
                        <option value="nl">DUTCH</option>
                        <option value="fi">FINNISH</option>
                        <option value="ru">RUSSIAN</option>
                    </select>
                </div>

                <div class="filter-group">
                    <label class="filter-label">
                        COPYRIGHT STATUS
                        <span class="help" title="Filter by copyright status">?</span>
                    </label>
                    <div>
                        <label class="filter-checkbox">
                            <input type="checkbox" id="copyright-false" checked>
                            <span>PUBLIC DOMAIN</span>
                        </label>
                        <label class="filter-checkbox">
                            <input type="checkbox" id="copyright-true">
                            <span>COPYRIGHTED</span>
                        </label>
                        <label class="filter-checkbox">
                            <input type="checkbox" id="copyright-null">
                            <span>UNKNOWN</span>
                        </label>
                    </div>
                </div>

                <div class="filter-group">
                    <label class="filter-label">
                        SORT ORDER
                        <span class="help" title="Order results by">?</span>
                    </label>
                    <select id="sort-select" class="filter-select">
                        <option value="popular">POPULARITY (DOWNLOADS)</option>
                        <option value="asc">ID ASCENDING</option>
                        <option value="desc">ID DESCENDING</option>
                    </select>
                </div>
            </div>

            <div class="filter-actions">
                <button id="apply-filters" class="btn">
                    <i class="fas fa-play"></i> APPLY FILTERS
                </button>
                <button id="reset-filters" class="btn btn-secondary">
                    <i class="fas fa-redo"></i> RESET ALL
                </button>
            </div>

            <div class="panel-section" style="margin-top: 24px;">
                <div class="panel-title">
                    <i class="fas fa-info-circle"></i> IN-APP READER
                </div>
                <div style="font-size: 11px; color: var(--text-muted); line-height: 1.5;">
                    <p><strong>SEAMLESS READING:</strong> Read books directly in the app</p>
                    <p style="margin-top: 8px;"><strong>THEME SUPPORT:</strong> Respects your current light/dark theme preference
                    </p>
                    <p style="margin-top: 8px; color: var(--accent-secondary);">Click "READ IN APP" for any book
                    </p>
                </div>
            </div>
        </aside>

        <!-- Main Content Area -->
        <main class="main-content">
            <!-- Search Controls -->
            <div class="search-controls">
                <div class="search-panel">
                    <div class="search-panel-title">
                        <i class="fas fa-search"></i> TITLE / AUTHOR SEARCH
                    </div>
                    <div class="search-input-group">
                        <input type="text" id="title-search" class="search-input"
                            placeholder="Search titles, authors..." autocomplete="off">
                        <button id="title-search-btn" class="btn" style="width: auto; min-width: 80px;">
                            <i class="fas fa-search"></i> SEARCH
                        </button>
                    </div>
                    <div class="search-examples">
                        Examples:
                        <span onclick="setSearchExample('title', 'Dickens')">Dickens</span>
                        <span onclick="setSearchExample('title', 'Frankenstein')">Frankenstein</span>
                        <span onclick="setSearchExample('title', 'Shakespeare')">Shakespeare</span>
                    </div>
                </div>

                <div class="search-panel">
                    <div class="search-panel-title">
                        <i class="fas fa-tags"></i> TOPIC / SUBJECT SEARCH
                    </div>
                    <div class="search-input-group">
                        <input type="text" id="topic-search" class="search-input"
                            placeholder="Search subjects, bookshelves..." autocomplete="off">
                        <button id="topic-search-btn" class="btn"
                            style="width: auto; min-width: 80px; background-color: var(--accent-secondary);">
                            <i class="fas fa-tags"></i> SEARCH
                        </button>
                    </div>
                    <div class="search-examples">
                        Examples:
                        <span onclick="setSearchExample('topic', 'Fiction')">Fiction</span>
                        <span onclick="setSearchExample('topic', 'Science')">Science</span>
                        <span onclick="setSearchExample('topic', 'Children')">Children</span>
                        <span onclick="setSearchExample('topic', 'Horror')">Horror</span>
                    </div>
                </div>
            </div>

            <!-- Combined Search Execution -->
            <div class="search-execute">
                <button id="combined-search-btn" class="btn" style="background-color: var(--accent-primary);">
                    <i class="fas fa-rocket"></i> EXECUTE COMBINED SEARCH
                </button>
            </div>

            <!-- Stats -->
            <div class="stats-container">
                <div class="stat-card">
                    <div class="stat-value" id="total-books">--</div>
                    <div class="stat-label">TOTAL RESULTS</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value" id="books-loaded">--</div>
                    <div class="stat-label">BOOKS LOADED</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value" id="current-page">--</div>
                    <div class="stat-label">CURRENT PAGE</div>
                </div>
                <div class="stat-card">
                    <div class="stat-value" id="total-pages">--</div>
                    <div class="stat-label">TOTAL PAGES</div>
                </div>
            </div>

            <!-- Active Search Terms -->
            <div id="active-searches" style="margin-bottom: 20px; display: none;">
                <div style="font-size: 11px; color: var(--text-muted); margin-bottom: 8px;">ACTIVE SEARCHES:</div>
                <div id="search-tags">
                    <!-- Search tags will be dynamically inserted here -->
                </div>
            </div>

            <!-- Content Grid -->
            <div class="books-grid" id="books-grid">
                <!-- Book cards will be dynamically inserted here -->
            </div>

            <!-- Pagination -->
            <div class="pagination">
                <button id="prev-page" class="btn-icon" disabled>
                    <i class="fas fa-chevron-left"></i>
                </button>
                <div class="page-info">
                    PAGE <span id="page-num" class="mono">1</span> OF <span id="page-total" class="mono">1</span>
                </div>
                <button id="next-page" class="btn-icon" disabled>
                    <i class="fas fa-chevron-right"></i>
                </button>
            </div>
        </main>
    </div>

    <!-- Book Details Modal -->
    <div class="modal" id="book-modal">
        <div class="modal-content">
            <div class="modal-header">
                <div class="modal-title" id="modal-title">BOOK DETAILS</div>
                <button class="close-modal" id="close-modal">&times;</button>
            </div>
            <div class="modal-body" id="modal-body">
                <!-- Book details will be dynamically inserted here -->
            </div>
        </div>
    </div>

    <!-- Dark Mode Reader Modal -->
    <div class="reader-modal" id="reader-modal">
        <div class="reader-content">
            <div class="reader-header">
                <div class="reader-title" id="reader-title">In-App Reader</div>
                <div class="reader-controls">
                    <button id="close-reader" class="btn" style="padding: 6px 12px; font-size: 11px;">
                        <i class="fas fa-times"></i> CLOSE
                    </button>
                </div>
            </div>
            <div class="reader-body" id="reader-body">
                <!-- Book content will be loaded here -->
            </div>
        </div>
    </div>

    <script>
        // Torus Loader Factory
        window.startTorusLoader = function(containerElement) {
            const wrap = document.createElement('div');
            wrap.className = 'torus-loader';
            wrap.setAttribute('role', 'status');
            wrap.setAttribute('aria-label', 'Loading');
            
            const cv = document.createElement('canvas');
            wrap.appendChild(cv);
            
            const text = document.createElement('p');
            text.className = 'loading-text';
            text.textContent = 'LOADING...';
            wrap.appendChild(text);
            
            // Clear container and append
            containerElement.innerHTML = '';
            containerElement.appendChild(wrap);

            const ctx = cv.getContext('2d');
            const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;

            const P = 3, Q = 2, R = 1.0, r = 0.42, N = 900;
            const pts = [];
            for (let i = 0; i <= N; i++) {
                const t = (i / N) * Math.PI * 2;
                const cq = Math.cos(Q * t), sq = Math.sin(Q * t);
                const rad = R + r * cq;
                pts.push([rad * Math.cos(P * t), rad * Math.sin(P * t), r * sq]);
            }

            let target = 0, shown = 0, rotY = 0.6, rotX = -0.5, raf = null, size = 0, dpr = 1, finished = false;
            let autoInterval;

            function resize() {
                dpr = Math.min(devicePixelRatio || 1, 2);
                size = cv.clientWidth;
                if (size === 0) size = 100;
                cv.width = cv.height = Math.round(size * dpr);
            }

            function project(p) {
                let [x, y, z] = p;
                let cx = Math.cos(rotY), sx = Math.sin(rotY);
                [x, z] = [x * cx + z * sx, -x * sx + z * cx];
                cx = Math.cos(rotX); sx = Math.sin(rotX);
                [y, z] = [y * cx - z * sx, y * sx + z * cx];
                const d = 4, s = d / (d - z);
                const half = size * dpr / 2, scale = half * 0.62;
                return [half + x * s * scale, half - y * s * scale, z];
            }

            function draw(ts) {
                const dt = 1 / 60;
                if (!reduced) { rotY += 0.35 * dt; rotX = -0.5 + Math.sin(ts * 0.0004) * 0.25; }
                shown += (target - shown) * (reduced ? 1 : 0.06);

                const proj = pts.map(project);
                const w = cv.width;
                ctx.clearRect(0, 0, w, w);
                ctx.lineCap = 'round'; ctx.lineJoin = 'round';

                const css = getComputedStyle(document.documentElement);
                const INK = css.getPropertyValue('--accent-primary').trim() || '#4a1a26';
                const GHOST = css.getPropertyValue('--border-color').trim() || 'rgba(255, 255, 255, 0.10)';

                ctx.strokeStyle = GHOST; ctx.lineWidth = 1.5 * dpr;
                ctx.beginPath();
                proj.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
                ctx.stroke();

                const count = Math.floor(shown * N);
                const segs = [];
                for (let i = 0; i < count; i++) segs.push(i);
                segs.sort((a, b) => proj[a][2] - proj[b][2]);
                for (const i of segs) {
                    const a = proj[i], b = proj[i + 1];
                    const depth = (a[2] + r + R) / (2 * (r + R));
                    ctx.strokeStyle = INK;
                    ctx.globalAlpha = 0.35 + 0.65 * depth;
                    ctx.lineWidth = (1.2 + 3.2 * depth) * dpr;
                    ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
                }
                ctx.globalAlpha = 1;

                if (count > 0 && count < N) {
                    const [x, y, z] = proj[count];
                    const depth = (z + r + R) / (2 * (r + R));
                    ctx.fillStyle = INK;
                    ctx.beginPath(); ctx.arc(x, y, (2.5 + 3 * depth) * dpr, 0, Math.PI * 2); ctx.fill();
                }

                if (finished && shown > 0.995) { finish(); return; }
                raf = requestAnimationFrame(draw);
            }

            function finish() {
                cancelAnimationFrame(raf); raf = null;
                wrap.classList.add('is-out');
                setTimeout(() => {
                    if (wrap.parentNode) wrap.parentNode.removeChild(wrap);
                }, 400); // Wait for transition
            }

            resize();
            raf = requestAnimationFrame(draw);
            autoInterval = setInterval(() => { if (target < 0.9) target += (0.9 - target) * 0.12; }, 200);

            return {
                done: () => {
                    clearInterval(autoInterval);
                    target = 1; finished = true;
                }
            };
        };

        function syncTheme() {
            try {
                if (window.parent && window.parent.document && window.parent.document.documentElement) {
                    const isDark = window.parent.document.documentElement.classList.contains('dark');
                    document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
                }
            } catch (e) {}
        }
        window.addEventListener('load', syncTheme);
        window.addEventListener('message', syncTheme);
        setInterval(syncTheme, 500);

        // Global variables
        let currentPage = 1;
        let totalPages = 1;
        let totalBooks = 0;
        let booksLoaded = 0;
        let currentBooks = [];
        let currentFilters = {
            search: '',
            languages: '',
            copyright: 'false',
            sort: 'popular',
            topic: ''
        };

        const booksGrid = document.getElementById('books-grid');
        const loadingEl = document.getElementById('loading');
        const totalBooksEl = document.getElementById('total-books');
        const booksLoadedEl = document.getElementById('books-loaded');
        const currentPageEl = document.getElementById('current-page');
        const totalPagesEl = document.getElementById('total-pages');
        const pageNumEl = document.getElementById('page-num');
        const pageTotalEl = document.getElementById('page-total');
        const prevPageBtn = document.getElementById('prev-page');
        const nextPageBtn = document.getElementById('next-page');
        const applyFiltersBtn = document.getElementById('apply-filters');
        const resetFiltersBtn = document.getElementById('reset-filters');
        const bookModal = document.getElementById('book-modal');
        const closeModalBtn = document.getElementById('close-modal');
        const readerModal = document.getElementById('reader-modal');
        const readerTitle = document.getElementById('reader-title');
        const readerBody = document.getElementById('reader-body');
        const closeReaderBtn = document.getElementById('close-reader');
        const activeSearchesEl = document.getElementById('active-searches');
        const searchTagsEl = document.getElementById('search-tags');
        const titleSearchEl = document.getElementById('title-search');
        const titleSearchBtn = document.getElementById('title-search-btn');
        const topicSearchEl = document.getElementById('topic-search');
        const topicSearchBtn = document.getElementById('topic-search-btn');
        const combinedSearchBtn = document.getElementById('combined-search-btn');
        const languageSelect = document.getElementById('language-select');
        const copyrightFalse = document.getElementById('copyright-false');
        const copyrightTrue = document.getElementById('copyright-true');
        const copyrightNull = document.getElementById('copyright-null');
        const sortSelect = document.getElementById('sort-select');

        document.addEventListener('DOMContentLoaded', () => {
            loadBooks(currentPage);
            setupEventListeners();
            updateActiveSearchTags();
        });

        function setupEventListeners() {
            // Title/Author search
            titleSearchBtn.addEventListener('click', executeTitleSearch);
            titleSearchEl.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') executeTitleSearch();
            });

            topicSearchBtn.addEventListener('click', executeTopicSearch);
            topicSearchEl.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') executeTopicSearch();
            });

            combinedSearchBtn.addEventListener('click', executeCombinedSearch);

            applyFiltersBtn.addEventListener('click', applyFilters);

            resetFiltersBtn.addEventListener('click', resetAll);

            prevPageBtn.addEventListener('click', () => {
                if (currentPage > 1) {
                    currentPage--;
                    loadBooks(currentPage);
                }
            });

            nextPageBtn.addEventListener('click', () => {
                if (currentPage < totalPages) {
                    currentPage++;
                    loadBooks(currentPage);
                }
            });

            closeModalBtn.addEventListener('click', () => {
                bookModal.style.display = 'none';
            });

            closeReaderBtn.addEventListener('click', () => {
                readerModal.style.display = 'none';
                document.body.style.overflow = '';
                readerBody.innerHTML = '';
            });

            window.addEventListener('click', (e) => {
                if (e.target === bookModal) {
                    bookModal.style.display = 'none';
                }
                if (e.target === readerModal) {
                    readerModal.style.display = 'none';
                    document.body.style.overflow = '';
                    readerBody.innerHTML = '';
                }
            });
        }

        function executeTitleSearch() {
            currentFilters.search = titleSearchEl.value.trim();
            currentFilters.topic = ''; // Clear topic for title-only search
            topicSearchEl.value = '';
            currentPage = 1;
            updateActiveSearchTags();
            loadBooks(currentPage);
        }

        function executeTopicSearch() {
            currentFilters.topic = topicSearchEl.value.trim();
            currentFilters.search = ''; // Clear title search for topic-only search
            titleSearchEl.value = '';
            currentPage = 1;
            updateActiveSearchTags();
            loadBooks(currentPage);
        }

        function executeCombinedSearch() {
            currentFilters.search = titleSearchEl.value.trim();
            currentFilters.topic = topicSearchEl.value.trim();
            currentPage = 1;
            updateActiveSearchTags();
            loadBooks(currentPage);
        }

        function applyFilters() {
            currentFilters.languages = languageSelect.value;

            let copyrightValues = [];
            if (copyrightFalse.checked) copyrightValues.push('false');
            if (copyrightTrue.checked) copyrightValues.push('true');
            if (copyrightNull.checked) copyrightValues.push('null');
            currentFilters.copyright = copyrightValues.join(',');

            currentFilters.sort = sortSelect.value;

            currentPage = 1;
            loadBooks(currentPage);
        }

        function resetAll() {
            // Reset search inputs
            titleSearchEl.value = '';
            topicSearchEl.value = '';

            languageSelect.value = '';
            copyrightFalse.checked = true;
            copyrightTrue.checked = false;
            copyrightNull.checked = false;
            sortSelect.value = 'popular';

            currentFilters = {
                search: '',
                languages: '',
                copyright: 'false',
                sort: 'popular',
                topic: ''
            };

            currentPage = 1;
            updateActiveSearchTags();
            loadBooks(currentPage);
        }

        function updateActiveSearchTags() {
            searchTagsEl.innerHTML = '';

            if (currentFilters.search) {
                const tag = document.createElement('div');
                tag.className = 'tag';
                tag.style.backgroundColor = 'rgba(61, 90, 254, 0.2)';
                tag.style.borderColor = 'var(--accent-primary)';
                tag.innerHTML = `TITLE/AUTHOR: ${currentFilters.search} <span style="margin-left: 6px; cursor: pointer;" onclick="clearSearch('title')">×</span>`;
                searchTagsEl.appendChild(tag);
            }

            if (currentFilters.topic) {
                const tag = document.createElement('div');
                tag.className = 'tag';
                tag.style.backgroundColor = 'rgba(0, 229, 255, 0.2)';
                tag.style.borderColor = 'var(--accent-secondary)';
                tag.innerHTML = `TOPIC: ${currentFilters.topic} <span style="margin-left: 6px; cursor: pointer;" onclick="clearSearch('topic')">×</span>`;
                searchTagsEl.appendChild(tag);
            }

            if (currentFilters.languages) {
                const tag = document.createElement('div');
                tag.className = 'tag';
                tag.innerHTML = `LANGUAGE: ${currentFilters.languages.toUpperCase()} <span style="margin-left: 6px; cursor: pointer;" onclick="clearFilter('language')">×</span>`;
                searchTagsEl.appendChild(tag);
            }

            if (currentFilters.copyright && currentFilters.copyright !== 'false') {
                const tag = document.createElement('div');
                tag.className = 'tag';
                tag.innerHTML = `COPYRIGHT: ${currentFilters.copyright} <span style="margin-left: 6px; cursor: pointer;" onclick="clearFilter('copyright')">×</span>`;
                searchTagsEl.appendChild(tag);
            }

            activeSearchesEl.style.display =
                (currentFilters.search || currentFilters.topic || currentFilters.languages ||
                    (currentFilters.copyright && currentFilters.copyright !== 'false')) ? 'block' : 'none';
        }

        function clearSearch(type) {
            if (type === 'title') {
                currentFilters.search = '';
                titleSearchEl.value = '';
            } else if (type === 'topic') {
                currentFilters.topic = '';
                topicSearchEl.value = '';
            }
            currentPage = 1;
            updateActiveSearchTags();
            loadBooks(currentPage);
        }

        // Clear specific filter
        function clearFilter(type) {
            if (type === 'language') {
                currentFilters.languages = '';
                languageSelect.value = '';
            } else if (type === 'copyright') {
                currentFilters.copyright = 'false';
                copyrightFalse.checked = true;
                copyrightTrue.checked = false;
                copyrightNull.checked = false;
            }
            currentPage = 1;
            updateActiveSearchTags();
            loadBooks(currentPage);
        }

        // Set search example
        function setSearchExample(type, term) {
            if (type === 'title') {
                titleSearchEl.value = term;
                currentFilters.search = term;
                currentFilters.topic = '';
                topicSearchEl.value = '';
            } else if (type === 'topic') {
                topicSearchEl.value = term;
                currentFilters.topic = term;
                currentFilters.search = '';
                titleSearchEl.value = '';
            }
            currentPage = 1;
            updateActiveSearchTags();
            loadBooks(currentPage);
        }

        // Find text/plain format for reader
        function findTextFormat(formats) {
            // Check for text/plain formats
            const textKeys = Object.keys(formats).filter(key =>
                key.includes('text/plain') ||
                key.includes('.txt') ||
                formats[key].includes('.txt')
            );

            if (textKeys.length > 0) {
                return formats[textKeys[0]];
            }

            // Check for HTML as fallback
            const htmlKeys = Object.keys(formats).filter(key =>
                key.includes('text/html')
            );

            if (htmlKeys.length > 0) {
                return formats[htmlKeys[0]];
            }

            return null;
        }

        // Load and display book in in-app reader - UPDATED TO WORK WITH CORS
        async function loadBookInApp(bookId) {
            // Find the book in currentBooks
            const book = currentBooks.find(b => b.id === bookId);
            if (!book) {
                alert('Book not found in current results.');
                return;
            }

            const textUrl = findTextFormat(book.formats);

            if (!textUrl) {
                alert('No readable format available for this book.');
                return;
            }

            // Set reader title
            readerTitle.textContent = `In-App Reader: ${book.title}`;

            // Show loading in reader modal
            let loader = window.startTorusLoader(readerBody);
            
            // Show reader modal
            readerModal.style.display = 'block';
            document.body.style.overflow = 'hidden';

            try {
                // Try multiple approaches to get the content
                const content = await getBookContentWithFallbacks(textUrl, book);

                // Display in reader
                displayTextInApp(content, book.title, textUrl);

            } catch (error) {
                console.error('Error loading book:', error);

                // Final fallback: Show book information with direct links
                readerBody.innerHTML = `
                    <div style="text-align: center; padding: 40px; color: var(--text-light);">
                        <div style="max-width: 800px; margin: 0 auto;">
                            <h2 style="color: var(--text-white); margin-bottom: 20px;">${book.title}</h2>
                            <div style="background-color: var(--bg-card); padding: 25px; border-radius: var(--radius-md); margin: 20px 0; text-align: left;">
                                <h4 style="color: var(--accent-secondary); margin-bottom: 15px; display: flex; align-items: center; gap: 8px;">
                                    <i class="fas fa-book"></i> BOOK INFORMATION
                                </h4>
                                <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 20px;">
                                    <div>
                                        <div style="font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;">AUTHOR</div>
                                        <div style="color: var(--text-light); font-size: 14px;">${book.authors[0]?.name || 'Unknown'}</div>
                                    </div>
                                    <div>
                                        <div style="font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;">PUBLICATION YEAR</div>
                                        <div style="color: var(--text-light); font-size: 14px;">${book.authors[0]?.birth_year ? 'Circa ' + book.authors[0].birth_year : 'Unknown'}</div>
                                    </div>
                                    <div>
                                        <div style="font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;">LANGUAGES</div>
                                        <div style="color: var(--text-light); font-size: 14px;">${book.languages.map(l => l.toUpperCase()).join(', ')}</div>
                                    </div>
                                    <div>
                                        <div style="font-size: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;">DOWNLOADS</div>
                                        <div style="color: var(--text-light); font-size: 14px;">${book.download_count.toLocaleString()}</div>
                                    </div>
                                </div>

                                ${book.summaries && book.summaries.length > 0 ? `
                                <div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border-color);">
                                    <h5 style="color: var(--accent-primary); margin-bottom: 10px; display: flex; align-items: center; gap: 8px;">
                                        <i class="fas fa-align-left"></i> SUMMARY
                                    </h5>
                                    <p style="color: var(--text-light); font-size: 14px; line-height: 1.7; text-align: justify;">
                                        ${book.summaries[0]}
                                    </p>
                                </div>
                                ` : ''}
                            </div>

                            <div style="background-color: var(--bg-card); padding: 25px; border-radius: var(--radius-md); margin: 20px 0; text-align: left;">
                                <h4 style="color: var(--accent-secondary); margin-bottom: 15px; display: flex; align-items: center; gap: 8px;">
                                    <i class="fas fa-download"></i> ACCESS THIS BOOK
                                </h4>
                                <p style="color: var(--text-light); margin-bottom: 20px; line-height: 1.6;">
                                    Due to CORS restrictions, we cannot load the full book content directly in this reader.
                                    However, you can access it through the following options:
                                </p>

                                <div style="display: flex; flex-wrap: wrap; gap: 15px; margin-top: 20px;">
                                    <a href="${textUrl}" target="_blank" class="btn" style="background: linear-gradient(135deg, #6d28d9, #8b5cf6); display: inline-flex; min-width: 200px;">
                                        <i class="fas fa-external-link-alt"></i> OPEN TEXT VERSION
                                    </a>
                                    <a href="https://www.gutenberg.org/ebooks/${book.id}" target="_blank" class="btn" style="background: linear-gradient(135deg, #10b981, #34d399); display: inline-flex; min-width: 200px;">
                                        <i class="fas fa-book"></i> VIEW ON GUTENBERG
                                    </a>
                                </div>

                                <div style="margin-top: 25px; padding-top: 20px; border-top: 1px solid var(--border-color);">
                                    <h5 style="color: var(--accent-primary); margin-bottom: 10px; display: flex; align-items: center; gap: 8px;">
                                        <i class="fas fa-lightbulb"></i> DARK MODE TIP
                                    </h5>
                                    <p style="color: var(--text-light); font-size: 13px; line-height: 1.6;">
                                        For dark mode reading, you can:
                                    </p>
                                    <ul style="color: var(--text-light); font-size: 13px; line-height: 1.6; margin-left: 20px; margin-top: 10px;">
                                        <li>Use browser extensions like "Dark Reader"</li>
                                        <li>Enable dark mode in your browser settings</li>
                                        <li>Copy the text and paste into a dark mode text editor</li>
                                        <li>Use the HTML version with browser dark mode</li>
                                    </ul>
                                </div>
                            </div>
                        </div>
                    </div>
                `;
            } finally {
                if (loader) setTimeout(() => loader.done(), 300);
            }
        }

        // Try multiple methods to get book content
        async function getBookContentWithFallbacks(textUrl, book) {
            // Method 1: Try direct fetch (may fail due to CORS)
            try {
                const response = await fetch(textUrl);
                if (response.ok) {
                    return await response.text();
                }
            } catch (error) {
                console.log('Direct fetch failed, trying proxy...');
            }

            // Method 2: Try with a CORS proxy
            try {
                // Try our native Netlify function proxy first, then public fallbacks
                const proxies = [
                    `/.netlify/functions/proxy?url=${encodeURIComponent(textUrl)}`,
                    `https://api.allorigins.win/raw?url=${encodeURIComponent(textUrl)}`,
                    `https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(textUrl)}`
                ];

                for (const proxyUrl of proxies) {
                    try {
                        const controller = new AbortController();
                        const timeoutId = setTimeout(() => controller.abort(), 10000);

                        const response = await fetch(proxyUrl, {
                            signal: controller.signal
                        });

                        clearTimeout(timeoutId);

                        if (response.ok) {
                            return await response.text();
                        }
                    } catch (error) {
                        console.log(`Proxy ${proxyUrl} failed`);
                        continue;
                    }
                }
            } catch (error) {
                console.log('All proxies failed');
            }

            // Method 3: Return a sample of the book info
            return createBookSample(book);
        }

        // Create a book sample when we can't fetch the full content
        function createBookSample(book) {
            let sample = `TITLE: ${book.title}\n\n`;
            sample += `AUTHOR: ${book.authors[0]?.name || 'Unknown'}\n`;
            sample += `PUBLICATION YEAR: ${book.authors[0]?.birth_year ? 'Circa ' + book.authors[0].birth_year : 'Unknown'}\n`;
            sample += `LANGUAGES: ${book.languages.map(l => l.toUpperCase()).join(', ')}\n`;
            sample += `DOWNLOAD COUNT: ${book.download_count.toLocaleString()}\n\n`;

            if (book.summaries && book.summaries.length > 0) {
                sample += `SUMMARY:\n${book.summaries[0]}\n\n`;
            }

            if (book.subjects.length > 0) {
                sample += `SUBJECTS:\n${book.subjects.slice(0, 5).join('\n')}\n\n`;
            }

            sample += `\n---\nFULL BOOK AVAILABLE AT: https://www.gutenberg.org/ebooks/${book.id}\n`;
            sample += `TEXT VERSION: ${findTextFormat(book.formats) || 'Not available'}\n\n`;
            sample += `NOTE: Due to CORS restrictions, the full text cannot be loaded directly.\n`;
            sample += `Please visit the Project Gutenberg website to read the complete book.\n`;

            return sample;
        }

        // Display text in in-app reader
        function displayTextInApp(text, title, textUrl = '') {
            // Check if this is a sample (contains our marker)
            const isSample = text.includes('FULL BOOK AVAILABLE AT:');

            let formattedText = '';

            if (isSample) {
                // Format the sample differently
                formattedText = `
                    <div style="max-width: 800px; margin: 0 auto;">
                        <h1 style="color: var(--text-white); font-family: 'Inter', sans-serif; margin-bottom: 20px; text-align: center;">${title}</h1>
                        <div style="margin-bottom: 40px; color: var(--text-muted); font-size: 12px; font-style: italic; text-align: center;">
                            Book Preview • Project Gutenberg
                        </div>
                        <div style="white-space: pre-wrap; font-family: 'JetBrains Mono', monospace; line-height: 1.8; color: var(--text-light); text-align: left; background-color: var(--bg-card); padding: 25px; border-radius: var(--radius-md);">
                            ${text}
                        </div>
                        <div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--border-color); color: var(--text-muted); font-size: 11px; text-align: center;">
                            <p>Complete book available at Project Gutenberg.</p>
                            ${textUrl ? `<p><a href="${textUrl}" target="_blank" style="color: var(--accent-primary);">Open full text version</a></p>` : ''}
                        </div>
                    </div>
                `;
            } else {
                // Process text for better display (limit for performance)
                let rawText = text.replace(/\r\n/g, '\n');
                let lines = rawText.split('\n');
                let processedLines = [];
                
                let i = 0;
                while (i < lines.length) {
                    let currentPara = lines[i];
                    
                    while (i < lines.length - 1) {
                        let l1 = lines[i];
                        let l2 = lines[i+1];
                        
                        if (l1.trim().length === 0 || l2.trim().length === 0) {
                            break; // Paragraph break
                        }
                        
                        let trimL1 = l1.trimEnd();
                        let trimL2 = l2.trimStart();
                        
                        let endsWithHyphen = trimL1.endsWith('-') && !trimL1.endsWith('--');
                        let nextStartsLowercase = /^[a-z]/.test(trimL2);
                        let isLong = trimL1.length > 55;
                        let isListOrTOC = trimL1.match(/(\.\.\.|[0-9])$/);
                        let nextIsIndented = /^[ \t]+/.test(l2);
                        
                        let isSoftWrap = endsWithHyphen || nextStartsLowercase || (isLong && !nextIsIndented && !isListOrTOC);
                        
                        if (isSoftWrap) {
                            if (endsWithHyphen) {
                                currentPara = currentPara.substring(0, currentPara.lastIndexOf('-')) + trimL2;
                            } else {
                                currentPara = currentPara + ' ' + trimL2;
                            }
                            i++; // Move to next line to consider it part of currentPara
                        } else {
                            break; // Stop merging
                        }
                    }
                    processedLines.push(currentPara);
                    i++;
                }
                
                let processedText = processedLines.join('\n')
                    .replace(/\n{3,}/g, '\n\n') // Limit multiple newlines
                    .substring(0, 50000); // Limit for performance

                formattedText = `
                    <div style="max-width: 800px; margin: 0 auto;">
                        <h1 style="color: var(--text-white); font-family: 'Inter', sans-serif; margin-bottom: 20px; text-align: center;">${title}</h1>
                        <div style="margin-bottom: 40px; color: var(--text-muted); font-size: 12px; font-style: italic; text-align: center;">
                            In-Browser Reader • Project Gutenberg
                        </div>
                        <div style="white-space: pre-wrap; font-family: 'Georgia', serif; line-height: 1.8; color: var(--text-light); text-align: justify;">
                            ${processedText}
                        </div>
                        <div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--border-color); color: var(--text-muted); font-size: 11px; text-align: center;">
                            <p>Preview of book content. Full book available at Project Gutenberg.</p>
                            ${textUrl ? `<p><a href="${textUrl}" target="_blank" style="color: var(--accent-primary);">Continue reading full version</a></p>` : ''}
                        </div>
                    </div>
                `;
            }

            readerBody.innerHTML = formattedText;
        }

        // Load books from API
        async function loadBooks(page) {
            // Show loading in grid
            let loader = window.startTorusLoader(booksGrid);

            // Build query parameters
            let params = new URLSearchParams();
            params.append('page', page);

            if (currentFilters.search) params.append('search', currentFilters.search);
            if (currentFilters.languages) params.append('languages', currentFilters.languages);
            if (currentFilters.copyright) params.append('copyright', currentFilters.copyright);
            if (currentFilters.sort) params.append('sort', currentFilters.sort);
            if (currentFilters.topic) params.append('topic', currentFilters.topic);

            try {
                // Fetch books from Gutendex API
                const response = await fetch(`https://gutendex.com/books/?${params.toString()}`);
                const data = await response.json();

                // Update global variables
                currentBooks = data.results || [];
                totalBooks = data.count || 0;
                booksLoaded = currentBooks.length;

                // Calculate total pages (32 books per page)
                totalPages = Math.ceil(totalBooks / 32) || 1;

                // Update UI
                updateStats();
                renderBooks();

            } catch (error) {
                console.error('Error fetching books:', error);
                booksGrid.innerHTML = `
                    <div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--accent-danger);">
                        <i class="fas fa-exclamation-triangle" style="font-size: 32px; margin-bottom: 12px;"></i>
                        <h3 style="margin-bottom: 8px;">API CONNECTION ERROR</h3>
                        <p style="color: var(--text-muted); margin-bottom: 16px;">Failed to fetch data from Gutendex API.</p>
                        <button class="btn" onclick="loadBooks(currentPage)" style="margin-top: 8px;">
                            <i class="fas fa-redo"></i> RETRY CONNECTION
                        </button>
                    </div>
                `;
            } finally {
                if (loader) setTimeout(() => loader.done(), 300);
            }
        }

        // Update statistics display
        function updateStats() {
            totalBooksEl.textContent = totalBooks.toLocaleString();
            booksLoadedEl.textContent = booksLoaded;
            currentPageEl.textContent = currentPage;
            totalPagesEl.textContent = totalPages;
            pageNumEl.textContent = currentPage;
            pageTotalEl.textContent = totalPages;

            // Update pagination button states
            prevPageBtn.disabled = currentPage <= 1;
            nextPageBtn.disabled = currentPage >= totalPages;
        }

        // Render books to the grid
        function renderBooks() {
            booksGrid.innerHTML = '';

            if (currentBooks.length === 0) {
                booksGrid.innerHTML = `
                    <div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: var(--text-muted);">
                        <i class="fas fa-search" style="font-size: 32px; margin-bottom: 12px;"></i>
                        <h3 style="margin-bottom: 8px;">NO BOOKS FOUND</h3>
                        <p>Try adjusting your search criteria or filters.</p>
                        <div style="margin-top: 16px; font-size: 11px; color: var(--accent-secondary);">
                            <p>Current search: ${currentFilters.search ? `"${currentFilters.search}"` : ''} ${currentFilters.topic ? `Topic: "${currentFilters.topic}"` : ''}</p>
                        </div>
                    </div>
                `;
                return;
            }

            currentBooks.forEach(book => {
                const bookCard = document.createElement('div');
                bookCard.className = 'book-card';
                bookCard.dataset.id = book.id;

                // Get first author name
                const authorName = book.authors.length > 0 ? book.authors[0].name : 'Unknown Author';

                // Get first subject
                const firstSubject = book.subjects.length > 0 ?
                    book.subjects[0].split('--')[0].trim() :
                    'No subjects listed';

                // Format languages
                const languages = book.languages.map(lang => lang.toUpperCase()).join(', ');

                bookCard.innerHTML = `
                    <div class="book-header">
                        <div>
                            <div class="book-title">${book.title}</div>
                            <div class="book-author">${authorName}</div>
                        </div>
                        <div class="book-id">ID: ${book.id}</div>
                    </div>
                    <div class="book-body">
                        <div style="font-size: 11px; color: var(--text-muted); margin-bottom: 10px; line-height: 1.5;">
                            ${book.summaries && book.summaries.length > 0 ?
                        book.summaries[0].substring(0, 120) + '...' :
                        firstSubject}
                        </div>
                        <div class="book-meta">
                            <div class="meta-item">
                                <div class="meta-value">${book.download_count.toLocaleString()}</div>
                                <div class="meta-label">DOWNLOADS</div>
                            </div>
                            <div class="meta-item">
                                <div class="meta-value">${languages}</div>
                                <div class="meta-label">LANGUAGE${book.languages.length > 1 ? 'S' : ''}</div>
                            </div>
                            <div class="meta-item">
                                <div class="meta-value">${book.subjects.length}</div>
                                <div class="meta-label">SUBJECTS</div>
                            </div>
                        </div>
                    </div>
                `;

                // Add click event to show book details
                bookCard.addEventListener('click', () => showBookDetails(book.id));

                booksGrid.appendChild(bookCard);
            });
        }

        // Show book details in modal
        function showBookDetails(bookId) {
            // Find the book in currentBooks
            const book = currentBooks.find(b => b.id === bookId);
            if (!book) return;

            // Update modal title
            document.getElementById('modal-title').textContent = book.title;

            // Build modal content
            const modalBody = document.getElementById('modal-body');

            // Format authors
            const authorsHtml = book.authors.map(author => {
                const years = author.birth_year || author.death_year ?
                    `(${author.birth_year || '?'} - ${author.death_year || '?'})` : '';
                return `<div>${author.name} ${years}</div>`;
            }).join('');

            // Format subjects
            const subjectsHtml = book.subjects.map(subject => {
                return `<span class="tag">${subject}</span>`;
            }).join('');

            // Format bookshelves
            const bookshelvesHtml = book.bookshelves.map(shelf => {
                return `<span class="tag">${shelf}</span>`;
            }).join('');

            // Format formats
            const formatsHtml = Object.entries(book.formats).slice(0, 6).map(([format, url]) => {
                const formatName = format.split(';')[0].split('/')[1] || format.split('/').pop() || format;
                return `<a href="${url}" target="_blank" class="tag format-tag">${formatName.toUpperCase()}</a>`;
            }).join('');

            // Build modal content
            modalBody.innerHTML = `
                <div class="modal-grid">
                    <div class="modal-item">
                        <div class="modal-label">AUTHORS</div>
                        <div class="modal-value">${authorsHtml || 'Unknown'}</div>
                    </div>
                    <div class="modal-item">
                        <div class="modal-label">PUBLICATION YEAR</div>
                        <div class="modal-value">${book.authors[0]?.birth_year ? 'Circa ' + book.authors[0].birth_year : 'Unknown'}</div>
                    </div>
                    <div class="modal-item">
                        <div class="modal-label">LANGUAGES</div>
                        <div class="modal-value">${book.languages.map(l => l.toUpperCase()).join(', ')}</div>
                    </div>
                    <div class="modal-item">
                        <div class="modal-label">DOWNLOADS</div>
                        <div class="modal-value">${book.download_count.toLocaleString()}</div>
                    </div>
                </div>

                ${book.summaries && book.summaries.length > 0 ? `
                <div class="modal-section">
                    <div class="modal-section-title">
                        <i class="fas fa-align-left"></i> SUMMARY
                    </div>
                    <div class="modal-value" style="line-height: 1.6;">${book.summaries[0]}</div>
                </div>
                ` : ''}

                ${book.subjects.length > 0 ? `
                <div class="modal-section">
                    <div class="modal-section-title">
                        <i class="fas fa-tags"></i> SUBJECTS
                    </div>
                    <div class="tags-container">${subjectsHtml}</div>
                </div>
                ` : ''}

                ${book.bookshelves.length > 0 ? `
                <div class="modal-section">
                    <div class="modal-section-title">
                        <i class="fas fa-bookmark"></i> BOOKSHELVES
                    </div>
                    <div class="tags-container">${bookshelvesHtml}</div>
                </div>
                ` : ''}

                <div class="modal-section">
                    <div class="modal-section-title">
                        <i class="fas fa-book-reader"></i> IN-APP READER
                    </div>

                    <div class="format-buttons">
                        <button onclick="loadBookInApp(${book.id})" class="app-reader-btn">
                            <i class="fas fa-book-open"></i> READ IN APP
                        </button>
                        <a href="https://www.gutenberg.org/ebooks/${book.id}" target="_blank" class="text-reader-btn">
                            <i class="fas fa-external-link-alt"></i> VIEW ON GUTENBERG
                        </a>
                    </div>

                    <div style="margin-top: 16px;">
                        <div class="modal-section-title">
                            <i class="fas fa-download"></i> AVAILABLE FORMATS
                        </div>
                        <div class="tags-container">${formatsHtml}</div>
                    </div>
                </div>
            `;

            // Show modal
            bookModal.style.display = 'block';
        }

        // Initial load
        loadBooks(currentPage);
    </script>
</body>

</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.