/ src / routes / +page.svelte
+page.svelte
  1  <script lang="ts">
  2    import ResultsList from './components/ResultsList.svelte';
  3    import type { SearchResult } from '$lib/types';
  4    import { onMount } from 'svelte';
  5    import { searchParams } from '$lib/stores';
  6    import { page } from '$app/stores';
  7    import { get } from 'svelte/store';
  8    import { browser } from '$app/environment';
  9    
 10    // Necesario para que TypeScript reconozca $page correctamente
 11    export {};
 12  
 13    let results: SearchResult[] = [];
 14    let query = '';
 15    let type: 'web' | 'images' | 'document' | 'app' = 'web';
 16    let language = 'all';
 17    let page_num = 0;
 18    let isSearching = false;
 19    let searchError = '';
 20    let totalResults = 0;
 21    let searchTime = 0;
 22    
 23    // Variables reactivas
 24    let pageUrl = new URL('http://localhost');
 25    let hasQuery = false;
 26    let isHomePage = true;
 27  
 28    let activeFilters = {
 29      resultadosPorPagina: 10,
 30      recurso: 'local', // Valores aceptados: 'local' o 'peer' (se mapea a 'global' en el servidor)
 31      preferMask: '',
 32      constraints: 'false', // Sin restricciones para resolver problemas de paginación
 33      mediaSearch: 'extended_strict'
 34    };
 35  
 36    // Determinar si estamos en la home
 37    $: {
 38      if ($page) {
 39        pageUrl = $page.url;
 40        hasQuery = pageUrl.searchParams.has('query');
 41        isHomePage = !hasQuery;
 42      }
 43    }
 44  
 45    let pageQuery = '';
 46    let pageType: 'web' | 'images' | 'document' | 'app' = 'web';
 47    let pageLanguage = 'all';
 48    
 49    // Actualización reactiva de los params de URL
 50    $: if (pageUrl) {
 51      pageQuery = pageUrl.searchParams.get('query') ?? '';
 52      pageType = (pageUrl.searchParams.get('type') as 'web' | 'images' | 'document' | 'app') ?? 'web';
 53      pageLanguage = pageUrl.searchParams.get('language') ?? 'all';
 54    }
 55  
 56    // Variable para evitar bucles infinitos
 57    let isInitialLoad = true;
 58    
 59    // Reaccionar a cambios de la URL
 60    $: if (hasQuery) {
 61      if ((query !== pageQuery || type !== pageType || language !== pageLanguage) && !isSearching) {
 62        console.log('URL actualizada, nueva búsqueda:', { pageQuery, pageType, pageLanguage });
 63        query = pageQuery;
 64        type = pageType;
 65        language = pageLanguage;
 66        page_num = 0;
 67        results = [];
 68        
 69        // Solo hacer la búsqueda si no es la carga inicial o si hay un query válido
 70        if (!isInitialLoad || (query && query.trim())) {
 71          console.log('Ejecutando búsqueda desde cambio de URL');
 72          fetchResults();
 73        }
 74        
 75        // Después de la primera ejecución, ya no es carga inicial
 76        isInitialLoad = false;
 77      }
 78    }
 79  
 80    async function fetchResults() {
 81      try {
 82        console.log('Iniciando búsqueda...');
 83        isSearching = true;
 84        searchError = '';
 85  
 86        // Construir parámetros de forma explícita siguiendo la documentación
 87        const params = new URLSearchParams();
 88        
 89        // Asegurarse que query no esté vacío
 90        if (!query || !query.trim()) {
 91          searchError = 'Por favor ingresa un término de búsqueda';
 92          isSearching = false;
 93          return;
 94        }
 95        
 96        // Parámetros obligatorios
 97        params.set('query', query.trim());
 98        params.set('type', type);
 99        params.set('language', language || 'all');
100        params.set('page', page_num.toString());
101        
102        // Parámetros opcionales de filtros avanzados
103        params.set('resultadosPorPagina', activeFilters.resultadosPorPagina.toString());
104        params.set('recurso', activeFilters.recurso || 'global'); // Usar global como valor por defecto para debugging
105        
106        // Solo añadir estos parámetros si tienen valor
107        if (activeFilters.preferMask) {
108          params.set('preferMask', activeFilters.preferMask);
109        }
110        
111        if (activeFilters.constraints) {
112          params.set('constraints', activeFilters.constraints);
113        }
114        
115        if (activeFilters.mediaSearch) {
116          params.set('mediaSearch', activeFilters.mediaSearch);
117        }
118        
119        console.log('Parámetros de búsqueda:', Object.fromEntries(params.entries()));
120  
121        // Construir la URL completa para debugging
122        console.log('URL completa para API proxy:', `/api/search?${params.toString()}`);
123        
124        // Usar un timeout más largo para debugging
125        const controller = new AbortController();
126        const timeout = setTimeout(() => controller.abort(), 20000);
127  
128        let res;
129        try {
130          // Intentar primero con nuestra API
131          console.log('Fetching desde API proxy:', `/api/search?${params.toString()}`);
132          res = await fetch(`/api/search?${params.toString()}`, { 
133            signal: controller.signal,
134            headers: {
135              'Accept': 'application/json',
136              'Cache-Control': 'no-cache'
137            }
138          });
139          clearTimeout(timeout);
140  
141          if (!res.ok) {
142            console.error(`Error HTTP: ${res.status}: ${res.statusText}`);
143            throw new Error(`Error ${res.status}: ${res.statusText}`);
144          }
145        } catch (fetchError) {
146          // Capturar errores de red aquí
147          console.error('Error de red:', fetchError);
148          searchError = fetchError.message || 'Error de red al conectar con el servidor. Verifique que YaCy esté en ejecución.';
149          isSearching = false;
150          return;
151        }
152  
153        // Si llegamos aquí, tenemos respuesta HTTP
154        let data;
155        try {
156          // Obtener texto para debug
157          const responseText = await res.text();
158          console.log('Respuesta cruda:', responseText.substring(0, 500) + '...');
159          
160          try {
161            // Intentar parsear JSON
162            data = JSON.parse(responseText);
163            console.log('Datos JSON recibidos:', data);
164          } catch (parseError) {
165            console.error('Error al parsear JSON:', parseError);
166            searchError = 'La respuesta no es un JSON válido';
167            isSearching = false;
168            return;
169          }
170        } catch (textError) {
171          console.error('Error al obtener texto de respuesta:', textError);
172          searchError = 'Error al leer la respuesta del servidor';
173          isSearching = false;
174          return;
175        }
176  
177        // Manejar errores en la respuesta
178        if (data.error || data.pagination_error) {
179          // Si hay un error de paginación específico de YaCy
180          if (data.pagination_error) {
181            console.warn('Error de paginación YaCy:', data.error_message);
182            searchError = data.error_message || 'Error en la paginación de resultados';
183            
184            // No intentamos más paginación en este caso
185            page_num--; // Retroceder a la página anterior
186            
187            // Finalizar búsqueda pero mantener resultados actuales
188            isSearching = false;
189            return;
190          }
191          // Otros errores generales
192          else if (data.error) {
193            console.warn('Error en la respuesta:', data.error);
194            
195            // Personalizar mensaje de error según el contexto
196            if (data.error.includes('YaCy') || data.error.toLowerCase().includes('timeout')) {
197              // Error de conexión con YaCy
198              searchError = 'No se pudo conectar con el servidor YaCy. Verifica que esté funcionando.';
199            } else if (data.error.includes('formato')) {
200              // Error de formato en la respuesta
201              searchError = 'La respuesta del servidor no tiene el formato esperado.';
202            } else {
203              // Otro tipo de error
204              searchError = data.error || 'Error desconocido en la búsqueda';
205            }
206            
207            // Si no hay resultados, terminar
208            if (!data.results || !Array.isArray(data.results) || data.results.length === 0) {
209              console.log('No hay resultados debido a un error:', searchError);
210              isSearching = false;
211              results = [];
212              return;
213            }
214            
215            // Si hay resultados a pesar del error, continuar pero mostrar el error
216            console.log('Hay resultados a pesar del error, continuando...');
217          }
218        }
219  
220        // Verificar si la respuesta tiene la estructura esperada
221        if (!data.results) {
222          console.warn('Estructura de respuesta inesperada:', data);
223          
224          // Intentar extraer resultados de la estructura canónica de YaCy si existe
225          if (data.channels && Array.isArray(data.channels) && data.channels.length > 0 && 
226              data.channels[0].items && Array.isArray(data.channels[0].items)) {
227            console.log('Adaptando estructura de canales de YaCy');
228            
229            try {
230              // Adaptar la estructura de YaCy a nuestro formato
231              const channel = data.channels[0];
232              
233              // Validar cada campo necesario
234              if (!channel.items || !Array.isArray(channel.items)) {
235                throw new Error('Canal sin items válidos');
236              }
237              
238              // Crear estructura compatible
239              data = {
240                results: channel.items.map((item: any) => {
241                  // Validar que el item tenga link
242                  if (!item.link) {
243                    console.warn('Item sin link:', item);
244                    item.link = '#';
245                  }
246                  
247                  // Crear objeto compatible
248                  return {
249                    title: item.title || 'Sin título',
250                    link: item.link,
251                    description: item.description || item.snippet || '',
252                    type: type,
253                    domain: item.link !== '#' ? new URL(item.link).hostname.replace(/^www\./, '') : '',
254                    formattedUrl: item.link
255                  };
256                }),
257                totalResults: parseInt(channel.totalResults || '0', 10),
258                searchTime: 0.5,
259                query: query,
260                page: page_num
261              };
262            } catch (adaptError) {
263              console.error('Error adaptando estructura:', adaptError);
264              searchError = 'Error al procesar la respuesta de YaCy';
265              isSearching = false;
266              results = [];
267              return;
268            }
269          } else {
270            // No se pudo adaptar, devolver error
271            searchError = 'La respuesta no tiene el formato esperado';
272            isSearching = false;
273            results = [];
274            return;
275          }
276        }
277  
278        if (!Array.isArray(data.results)) {
279          console.error('data.results no es un array:', data.results);
280          searchError = 'El formato de respuesta es inválido';
281          isSearching = false;
282          return;
283        }
284  
285        console.log(`Procesando ${data.results.length} resultados`);
286        
287        // Procesar resultados
288        if (data.results.length === 0) {
289          console.log(`No hay resultados disponibles para la página ${page_num}`);
290          
291          // Si es la primera página, reemplazar completamente los resultados
292          if (page_num === 0) {
293            results = [];
294            searchError = 'No se encontraron resultados para tu búsqueda';
295          } 
296          // Si no es la primera página, mantener resultados anteriores
297          else {
298            searchError = 'No hay más resultados disponibles';
299          }
300        } else {
301          // Páginas adicionales - añadir manteniendo unicidad
302          const existingLinks = new Set(results.map(r => r.link));
303          const uniqueNewResults = data.results.filter(r => !existingLinks.has(r.link));
304          
305          // Añadir solo si hay nuevos resultados
306          if (uniqueNewResults.length > 0) {
307            results = [...results, ...uniqueNewResults];
308          } else if (data.results.length > 0) {
309            console.log('Se encontraron resultados pero todos ya existían en la lista');
310          }
311        }
312  
313        searchTime = data.searchTime ?? 0;
314        totalResults = data.totalResults ?? 0;
315        
316        // Éxito - finalizar la búsqueda
317        isSearching = false;
318        console.log('Búsqueda completada con éxito:', { 
319          resultCount: results.length, 
320          totalResults, 
321          searchTime 
322        });
323  
324      } catch (error) {
325        console.error('Error en búsqueda:', error);
326        const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
327        
328        if (error instanceof Error && error.name === 'AbortError') {
329          searchError = 'La búsqueda tardó demasiado tiempo. Verifica que YaCy esté funcionando en http://localhost:8090.';
330        } else if (errorMessage.includes('YaCy')) {
331          searchError = `Error al conectar con YaCy. ${errorMessage}`;
332        } else {
333          searchError = errorMessage;
334        }
335  
336        if (page_num === 0) results = [];
337        
338        // Error - finalizar la búsqueda
339        isSearching = false;
340      } finally {
341        // Asegurarse de que isSearching siempre se resetea
342        setTimeout(() => {
343          if (isSearching) {
344            console.warn('isSearching no se reseteo correctamente, forzando reset');
345            isSearching = false;
346          }
347        }, 5000);
348      }
349    }
350  
351    async function loadMore() {
352      if (isSearching || !query.trim()) return;
353      
354      // Evitar cargar más páginas si ya hemos llegado al límite de resultados
355      // Asumiendo que cada página muestra activeFilters.resultadosPorPagina resultados
356      const maxPages = totalResults ? Math.ceil(totalResults / activeFilters.resultadosPorPagina) : 100;
357      
358      if (page_num >= maxPages - 1) {
359        console.log(`Ya estamos en la última página (${page_num+1} de ${maxPages}) según totalResults=${totalResults}`);
360        searchError = 'Has alcanzado el final de los resultados disponibles';
361        return;
362      }
363  
364      const prevScrollPos = window.scrollY;
365      const prevResultsCount = results.length;
366      page_num++;
367  
368      await fetchResults();
369  
370      const newResultsCount = results.length - prevResultsCount;
371  
372      if (type === 'images') {
373        showScrollIndicator(newResultsCount);
374        window.scrollTo({ top: prevScrollPos, behavior: 'auto' });
375      } else {
376        const newResults = document.querySelectorAll(`.result-card:nth-child(n+${prevResultsCount + 1})`);
377        if (newResults.length > 0) {
378          const offset = 100;
379          const topPos = (newResults[0] as HTMLElement).offsetTop - offset;
380          window.scrollTo({ top: topPos, behavior: 'smooth' });
381        }
382      }
383    }
384  
385    function showScrollIndicator(count: number) {
386      const indicator = document.createElement('div');
387      indicator.textContent = `Cargados ${count} resultados nuevos`;
388      indicator.style.cssText = `
389        position: fixed;
390        bottom: 20px;
391        left: 50%;
392        transform: translateX(-50%);
393        background: var(--primary);
394        color: white;
395        padding: 8px 16px;
396        border-radius: 20px;
397        z-index: 1001;
398        font-size: 14px;
399        box-shadow: 0 2px 8px rgba(0,0,0,0.2);
400        opacity: 0;
401        transition: opacity 0.3s ease;
402      `;
403      document.body.appendChild(indicator);
404      setTimeout(() => {
405        indicator.style.opacity = '1';
406        setTimeout(() => {
407          indicator.style.opacity = '0';
408          setTimeout(() => indicator.remove(), 300);
409        }, 3000);
410      }, 100);
411    }
412  
413    onMount(() => {
414      const urlParams = new URLSearchParams(window.location.search);
415      const urlQuery = urlParams.get('query');
416  
417      if (urlQuery) {
418        query = urlQuery;
419        type = (urlParams.get('type') as 'web' | 'images' | 'document' | 'app') ?? 'web';
420        language = urlParams.get('language') ?? 'all';
421        page_num = 0;
422        results = [];
423        fetchResults();
424      } else {
425        fetch('/api/yacy/search')
426          .then(res => res.ok ? console.log('YaCy está listo') : console.warn('YaCy no responde'))
427          .catch(() => console.warn('No se pudo verificar YaCy'));
428      }
429    });
430  
431    searchParams.subscribe(params => {
432      if (params.query) {
433        query = params.query;
434        type = params.type as 'web' | 'images' | 'document' | 'app';
435        language = params.language;
436        page_num = 0;
437        results = [];
438        fetchResults();
439      }
440    });
441  </script>
442  
443  
444  
445  <!-- Contenedor principal con clases condicionales basadas en si es página de inicio o resultados -->
446  <div class="main-container {isHomePage ? 'home-view' : 'results-view'}">
447    <!-- Eliminado el contenido duplicado del texto de la página principal -->
448    <!-- Ahora solo se muestra el contenido desde +layout.svelte -->
449    
450    <!-- Eliminados controles de vista -->
451  
452    <!-- Página de resultados de búsqueda -->
453    {#if query && !isHomePage}
454      <section class="results-section">
455        <!-- Cabecera de resultados -->
456        <div class="results-header">
457          {#if isSearching && page_num === 0}
458            <div class="results-status">
459              <div class="loader"></div>
460              <p>Buscando resultados para "{query}"...</p>
461            </div>
462          {:else if searchError}
463            <div class="results-error">
464              <div class="error-icon">
465                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
466                  <circle cx="12" cy="12" r="10"></circle>
467                  <line x1="12" y1="8" x2="12" y2="12"></line>
468                  <line x1="12" y1="16" x2="12.01" y2="16"></line>
469                </svg>
470              </div>
471              <div class="error-content">
472                <h3>Error al realizar la búsqueda</h3>
473                <p>{searchError}</p>
474                <div class="error-help">
475                  <a href="http://127.0.0.1:8090" target="_blank" rel="noopener">
476                    Verificar estado de YaCy →
477                  </a>
478                </div>
479                <div class="error-actions">
480                  <button class="retry-button" on:click={() => fetchResults()}>
481                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
482                      <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
483                    </svg>
484                    <span>Reintentar</span>
485                  </button>
486                  <!-- Sin modo debug -->
487                </div>
488              </div>
489            </div>
490          {:else if results.length > 0}
491            <div class="results-info">
492              <p>Aproximadamente <strong>{totalResults.toLocaleString()}</strong> resultados <span class="search-time">({searchTime.toFixed(2)} segundos)</span></p>
493            </div>
494          {/if}
495        </div>
496        
497        <!-- Mostrar componente adecuado según el tipo de búsqueda -->
498        {#if type === 'maps'}
499          <div class="map-view-container">
500            <MapResults 
501              isMapMode={true}
502              searchQuery={query}
503            />
504          </div>
505        {:else}
506          <ResultsList 
507            {results} 
508            {loadMore}
509            searchType={type}
510            loading={isSearching && page_num > 0}
511          />
512        {/if}
513        
514        <!-- Sin panel de depuración -->
515      </section>
516    {:else if searchError && searchError.includes('YaCy')}
517      <!-- Mostrar cuando hay un error con YaCy -->
518      <div class="yacy-error-container">
519        <div class="yacy-error-icon">
520          <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
521            <circle cx="12" cy="12" r="10"></circle>
522            <line x1="12" y1="8" x2="12" y2="12"></line>
523            <line x1="12" y1="16" x2="12.01" y2="16"></line>
524          </svg>
525        </div>
526        <h2>No se pudo conectar con YaCy</h2>
527        <p>Asegúrate de que YaCy esté en ejecución en <a href="http://localhost:8090" target="_blank" rel="noopener noreferrer">http://localhost:8090</a></p>
528        <div class="yacy-error-actions">
529          <button class="retry-button large" on:click={() => fetchResults()}>
530            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
531              <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
532            </svg>
533            <span>Reintentar búsqueda</span>
534          </button>
535          <!-- Sin botón de datos de ejemplo -->
536          <!-- Sin modo debug -->
537        </div>
538      </div>
539    {:else if !isHomePage}
540      <!-- Mostrar cuando se navega a /search pero sin query -->
541      <div class="no-query-container">
542        <div class="no-query-icon">🔍</div>
543        <h2>Ingresa tu búsqueda arriba</h2>
544        <p>Escribe lo que deseas buscar en la barra de búsqueda</p>
545      </div>
546    {/if}
547  </div>
548  
549  <style>
550    /* Contenedor principal */
551    .main-container {
552      display: flex;
553      flex-direction: column;
554      width: 100%;
555      box-sizing: border-box;
556      position: relative;
557      overflow-x: hidden;
558    }
559  
560    /* Página de inicio */
561    .home-view {
562      min-height: calc(100vh - var(--header-height));
563      display: flex;
564      flex-direction: column;
565      justify-content: center;
566      padding: 0 var(--space-md);
567    }
568  
569    /* Estilos de la página de inicio movidos a +layout.svelte */
570  
571    /* Página de resultados */
572    .results-view {
573      min-height: calc(100vh - var(--header-height));
574      padding-top: 0; /* Eliminado el padding extra */
575      background-color: var(--bg-primary);
576    }
577    
578    /* Contenedor para la vista de mapas */
579    .map-view-container {
580      position: fixed;
581      width: 100vw;
582      height: calc(100vh - var(--header-height) - 50px); /* Ajustado para header + categorías */
583      top: calc(var(--header-height) + 50px); /* Posicionado debajo del header y categorías */
584      left: 0;
585      right: 0;
586      bottom: 0;
587      z-index: 50;
588      padding: 0;
589      margin: 0;
590    }
591  
592    .results-section {
593      padding: 0 var(--space-md);
594      max-width: var(--content-max-width);
595      margin: 0 auto;
596      width: 100%;
597      box-sizing: border-box;
598      /* Usa el color de fondo del tema */
599    }
600    
601    .results-header {
602      margin-bottom: var(--space-md);
603      padding-bottom: var(--space-sm);
604      padding-top: var(--space-sm);
605      border-bottom: 1px solid var(--border-color);
606    }
607    
608    .results-info {
609      font-size: 0.9rem;
610      color: var(--text-secondary);
611    }
612    
613    .results-info p {
614      margin: 0;
615    }
616    
617    .results-info strong {
618      color: var(--text-color);
619      font-weight: var(--font-weight-medium);
620    }
621    
622    .search-time {
623      font-size: 0.85rem;
624      opacity: 0.8;
625    }
626    
627    .results-status {
628      display: flex;
629      align-items: center;
630      gap: var(--space-sm);
631      color: var(--text-secondary);
632      font-size: 0.9rem;
633    }
634    
635    .loader {
636      width: 16px;
637      height: 16px;
638      border: 2px solid var(--border-color);
639      border-top-color: var(--primary);
640      border-radius: 50%;
641      animation: spin 0.8s linear infinite;
642    }
643    
644    @keyframes spin {
645      to { transform: rotate(360deg); }
646    }
647    
648    .results-error {
649      display: flex;
650      align-items: flex-start;
651      gap: var(--space-md);
652      padding: var(--space-md);
653      background-color: rgba(239, 68, 68, 0.07);
654      border-radius: var(--border-radius);
655      border: 1px solid rgba(239, 68, 68, 0.15);
656      margin-bottom: var(--space-lg);
657      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
658    }
659    
660    .error-icon {
661      color: var(--error);
662      flex-shrink: 0;
663    }
664    
665    .error-content {
666      flex: 1;
667    }
668    
669    .error-content h3 {
670      color: var(--error);
671      font-size: 1rem;
672      margin: 0 0 var(--space-xs);
673      font-weight: var(--font-weight-medium);
674    }
675    
676    .error-content p {
677      margin: 0 0 var(--space-md);
678      color: var(--text-color);
679      font-size: 0.9rem;
680      line-height: 1.5;
681    }
682    
683    .error-help {
684      margin-bottom: var(--space-sm);
685    }
686    
687    .error-help a {
688      color: var(--primary);
689      text-decoration: none;
690      font-size: 0.9rem;
691      display: inline-flex;
692      align-items: center;
693      gap: var(--space-xs);
694      font-weight: var(--font-weight-medium);
695    }
696    
697    .error-help a:hover {
698      text-decoration: underline;
699    }
700    
701    .error-actions {
702      display: flex;
703      gap: var(--space-sm);
704    }
705    
706    .retry-button {
707      display: flex;
708      align-items: center;
709      gap: var(--space-xs);
710      background: var(--bg-secondary);
711      border: 1px solid var(--border-color);
712      border-radius: var(--border-radius-sm);
713      color: var(--text-color);
714      padding: var(--space-xs) var(--space-sm);
715      font-size: 0.9rem;
716      cursor: pointer;
717      transition: all var(--transition-speed);
718      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
719    }
720    
721    .retry-button:hover {
722      background: var(--bg-primary);
723      border-color: var(--primary);
724      color: var(--primary);
725      box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
726    }
727    
728    .retry-button.large {
729      padding: var(--space-sm) var(--space-md);
730      font-size: 1rem;
731    }
732    
733    /* Error de YaCy y sin consulta */
734    .yacy-error-container,
735    .no-query-container {
736      display: flex;
737      flex-direction: column;
738      align-items: center;
739      justify-content: center;
740      text-align: center;
741      padding: var(--space-xxl) 0;
742      min-height: 50vh;
743      max-width: 600px;
744      margin: 0 auto;
745    }
746    
747    .yacy-error-icon {
748      margin-bottom: var(--space-lg);
749      color: var(--error);
750      opacity: 0.7;
751    }
752    
753    .yacy-error-container h2 {
754      margin: 0 0 var(--space-md);
755      font-size: 1.8rem;
756      font-weight: var(--font-weight-medium);
757      color: var(--text-color);
758      line-height: 1.3;
759    }
760    
761    .yacy-error-container p,
762    .no-query-container p {
763      margin: 0 0 var(--space-lg);
764      max-width: 500px;
765      line-height: 1.5;
766      color: var(--text-secondary);
767    }
768    
769    .yacy-error-container a {
770      color: var(--primary);
771      text-decoration: none;
772      font-weight: var(--font-weight-medium);
773    }
774    
775    .yacy-error-container a:hover {
776      text-decoration: underline;
777    }
778    
779    .yacy-error-actions {
780      display: flex;
781      gap: var(--space-md);
782      flex-wrap: wrap;
783      justify-content: center;
784    }
785    
786    .no-query-icon {
787      font-size: 3rem;
788      margin-bottom: var(--space-md);
789      opacity: 0.7;
790    }
791    
792    .no-query-container h2 {
793      margin: 0 0 var(--space-sm);
794      font-size: 1.5rem;
795      font-weight: var(--font-weight-medium);
796      color: var(--text-color);
797    }
798    
799  
800    /* Responsive */
801    /* Control de vista sticky */
802    .view-controls-sticky {
803      position: fixed;
804      left: var(--space-lg);
805      bottom: var(--space-lg);
806      z-index: 1000;
807      transition: transform 0.3s ease, opacity 0.3s ease;
808      animation: fade-in 0.5s ease-out;
809    }
810    
811    /* Animación para entrada suave */
812    @keyframes fade-in {
813      from { opacity: 0; transform: translateY(20px); }
814      to { opacity: 1; transform: translateY(0); }
815    }
816  
817    @media (max-width: 1024px) {
818      .results-section {
819        padding: 0 var(--space-lg);
820      }
821      
822      .view-controls-sticky {
823        left: var(--space-md);
824        bottom: var(--space-md);
825      }
826      
827      .home-logo h1 {
828        font-size: 2.2rem;
829      }
830      
831      .home-logo-img {
832        width: 100px;
833        height: 100px;
834      }
835    }
836    
837    @media (max-width: 768px) {
838      .results-section {
839        padding: 0 var(--space-md);
840      }
841      
842      .home-logo h1 {
843        font-size: 2rem;
844      }
845      
846      .home-logo-img {
847        width: 90px;
848        height: 90px;
849      }
850      
851      .home-tagline {
852        font-size: 1rem;
853      }
854      
855      .yacy-error-actions {
856        flex-direction: column;
857        gap: var(--space-sm);
858        width: 100%;
859        max-width: 300px;
860      }
861      
862      .retry-button.large, 
863      .mock-button.large,
864      .debug-button.large {
865        width: 100%;
866      }
867    }
868    
869    @media (max-width: 480px) {
870      .results-section {
871        padding: 0 var(--space-sm);
872      }
873      
874      .home-logo h1 {
875        font-size: 1.8rem;
876      }
877      
878      .home-logo-img {
879        width: 80px;
880        height: 80px;
881      }
882      
883      .home-tagline {
884        font-size: 0.9rem;
885        margin-bottom: var(--space-lg);
886      }
887      
888      .results-error {
889        flex-direction: column;
890        padding: var(--space-sm);
891      }
892      
893      .error-actions {
894        flex-direction: column;
895        gap: var(--space-xs);
896        width: 100%;
897      }
898      
899      .retry-button {
900        width: 100%;
901        justify-content: center;
902      }
903    }
904  </style>