Jose Salazar commited on
Commit
445de93
·
1 Parent(s): 178feb4

Deploy Morphos en HF Spaces

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.dockerignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sensitive files
2
+ .env
3
+ *.env
4
+
5
+ # SQLite database (will be created at runtime)
6
+ data/*.db
7
+
8
+ # OS
9
+ .DS_Store
10
+ Thumbs.db
11
+
12
+ # IDE
13
+ .vscode/
14
+ .idea/
15
+ *.swp
16
+ *.swo
17
+
18
+ # Git
19
+ .git/
20
+ .gitignore
21
+
22
+ # Node / build tools (none used, but just in case)
23
+ node_modules/
24
+
25
+ # Documentation not needed in the image
26
+ README.md
27
+ CLAUDE.md
28
+ SKILL.md
29
+ USO_DE_IA.md
.gitignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # General
2
+ .DS_Store
3
+ .localized
4
+ __MACOSX/
5
+ .AppleDouble
6
+ .LSOverride
7
+ Icon[]
8
+
9
+ # Thumbnails
10
+ ._*
11
+
12
+ # Files that might appear in the root of a volume
13
+ .DocumentRevisions-V100
14
+ .fseventsd
15
+ .Spotlight-V100
16
+ .TemporaryItems
17
+ .Trashes
18
+ .VolumeIcon.icns
19
+ .com.apple.timemachine.donotpresent
20
+
21
+ # Directories potentially created on remote AFP share
22
+ .AppleDB
23
+ .AppleDesktop
24
+ Network Trash Folder
25
+ Temporary Items
26
+ .apdisk
27
+
28
+
29
+ .env
.hf-skill-manifest.json ADDED
File without changes
.htaccess ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deny access to sensitive files
2
+ <FilesMatch "^(\.env|setup\.php)$">
3
+ Require all denied
4
+ </FilesMatch>
5
+
6
+ # Gzip compression
7
+ <IfModule mod_deflate.c>
8
+ AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json font/ttf font/woff font/woff2
9
+ </IfModule>
10
+
11
+ # Cache-Control headers
12
+ <IfModule mod_expires.c>
13
+ ExpiresActive On
14
+
15
+ # HTML — short cache, content can change
16
+ ExpiresByType text/html "access plus 1 hour"
17
+
18
+ # CSS and JS — 1 year (filenames are stable)
19
+ ExpiresByType text/css "access plus 1 year"
20
+ ExpiresByType application/javascript "access plus 1 year"
21
+ ExpiresByType text/javascript "access plus 1 year"
22
+
23
+ # Fonts — 1 year
24
+ ExpiresByType font/ttf "access plus 1 year"
25
+ ExpiresByType font/woff "access plus 1 year"
26
+ ExpiresByType font/woff2 "access plus 1 year"
27
+
28
+ # JSON data files
29
+ ExpiresByType application/json "access plus 1 day"
30
+ </IfModule>
31
+
32
+ <IfModule mod_headers.c>
33
+ <FilesMatch "\.(css|js|ttf|woff|woff2)$">
34
+ Header set Cache-Control "public, max-age=31536000, immutable"
35
+ </FilesMatch>
36
+ <FilesMatch "\.json$">
37
+ Header set Cache-Control "public, max-age=86400"
38
+ </FilesMatch>
39
+ </IfModule>
.vscode/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "liveServer.settings.port": 5501
3
+ }
CLAUDE.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Morphos is a veterinary diagnostic support tool — a single-page application (SPA) that performs real-time clinical pattern detection from lab values and optionally calls an AI model (HuggingFace or local Ollama) for clinical interpretation. It targets Canino and Felino patients.
8
+
9
+ ## Running the App
10
+
11
+ This is a static frontend with a PHP proxy backend. No build step required.
12
+
13
+ Serve it locally with PHP's built-in server from the project root:
14
+ ```bash
15
+ php -S localhost:8000
16
+ ```
17
+
18
+ Then open `http://localhost:8000` in a browser. The PHP proxy (`api/hf_proxy.php`) requires the API key in `api/.env`:
19
+ ```
20
+ HF_API_KEY=<your_key>
21
+ ```
22
+
23
+ For local AI inference, Ollama must be running at `http://localhost:11434` with `medgemma:latest` pulled.
24
+
25
+ ## Architecture
26
+
27
+ ### Data Flow
28
+
29
+ ```
30
+ User form input
31
+ → analisis.js (real-time pattern detection, no server)
32
+ → UI updates (color-coded fields, pattern cards)
33
+
34
+ User clicks "Análisis IA"
35
+ → ia.js (constructs prompt with patient data + flagged values)
36
+ → [HF route] → api/hf_proxy.php → HF Space Gradio API (SSE response)
37
+ → [Local route] → Ollama chat completions API
38
+ → Display AI output in #salida-ia
39
+ ```
40
+
41
+ ### Key Files and Their Roles
42
+
43
+ - **`js/analisis.js`** — Core engine (505 lines). Compares values against species-specific reference ranges, classifies severity (mild/moderate/severe), applies age/breed/sex adjustments, and identifies 50+ clinical patterns (anemia types, hepatic, renal, endocrine, etc.).
44
+ - **`js/ia.js`** — AI abstraction layer. Builds the clinical prompt in Spanish, calls either HF Proxy or Ollama, and strips model-specific tokens from the response.
45
+ - **`js/main.js`** — Orchestration: loads JSON data files, wires form events, triggers analysis, handles PDF export.
46
+ - **`js/ui.js`** — Tab navigation (8 panels, 4 exam sub-tabs), swipe gestures, mobile/desktop field sync, collapsible panels.
47
+ - **`js/pdf-parser.js`** — Client-side PDF extraction using PDF.js. 47 regex patterns to identify analytes in Spanish/English. Runs fully in the browser.
48
+ - **`api/hf_proxy.php`** — PHP proxy that reads `api/.env`, forwards requests to HugginFace Space (`blackmistcode-morphos-medgemma.hf.space/gradio_api`), handles SSE polling, and returns `{text: ...}`.
49
+ - **`data/valores_referencia.json`** — Reference ranges for 34 analytes per species.
50
+ - **`data/alteraciones.json`** — 100+ clinical entities used to enrich AI prompts with etiologic context.
51
+
52
+ ### AI Backend Configuration
53
+
54
+ Stored in `localStorage`:
55
+ - `mx-ia-backend`: `"hf"` (default) or `"local"`
56
+ - `mx-ia-ollama-url`: custom Ollama endpoint
57
+ - `mx-ia-ollama-model`: custom model name (default `medgemma:latest`)
58
+
59
+ The HF route supports up to 4 images (vision model). The local route uses Ollama's OpenAI-compatible chat completions API, also with vision support.
60
+
61
+ ### Pattern Detection Logic (`analisis.js`)
62
+
63
+ Severity thresholds are based on deviation from the reference range. Reference ranges are dynamically adjusted for:
64
+ - **Age**: puppies, adults, seniors, geriatric (age in months)
65
+ - **Breed**: Greyhounds (lower platelets normal), Akita/Shiba (different RBC ranges), etc.
66
+ - **Sex**: Male felines have a higher creatinine tolerance
67
+
68
+ The `analizarResultados()` function is called on every `input` event and returns flagged findings + matched clinical patterns.
69
+
70
+ ### CSS Notes
71
+
72
+ Do not use `!important` — use specificity or cascade ordering instead. The stylesheet is `css/styles.css` (1796 lines). The desktop grid breakpoint is `>1100px`.
73
+
74
+ ### Coding notes
75
+
76
+ All variables should be named in spanish unless they're referencing common technical names like tab, input, output, etc.
77
+ Always use descriptive names for variables and functions keeping legibility as a priority.
78
+ Don't use aligment spaces.
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM php:8.2-apache
2
+
3
+ # Install extensions and enable Apache modules
4
+ RUN apt-get update && apt-get install -y \
5
+ libpng-dev \
6
+ libjpeg-dev \
7
+ libfreetype6-dev \
8
+ libzip-dev \
9
+ unzip \
10
+ && docker-php-ext-install pdo pdo_mysql pdo_sqlite \
11
+ && docker-php-ext-enable pdo pdo_mysql pdo_sqlite \
12
+ && a2enmod rewrite deflate headers expires
13
+
14
+ # Copy project
15
+ COPY . /var/www/html/
16
+
17
+ # Create data directory for SQLite and ensure permissions
18
+ RUN mkdir -p /var/www/html/data \
19
+ && chown -R www-data:www-data /var/www/html \
20
+ && chmod -R 755 /var/www/html
21
+
22
+ # Expose HF Spaces default port
23
+ RUN sed -i 's/Listen 80/Listen 7860/' /etc/apache2/ports.conf \
24
+ && sed -i 's/:80/:7860/' /etc/apache2/sites-available/000-default.conf
25
+
26
+ EXPOSE 7860
27
+
28
+ # Entrypoint handles runtime initialization
29
+ COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
30
+ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
31
+
32
+ ENTRYPOINT ["docker-entrypoint.sh"]
33
+ CMD ["apache2-foreground"]
LICENSE ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jose Salazar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
24
+
25
+
26
+
27
+
28
+
README.md CHANGED
@@ -1,12 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Morphos
3
- emoji: 🦀
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: Morphos - Interprete de Analíticas veterinarias
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Morphos — Intérprete de analíticas veterinarias asistido por I.A
2
+ ## Proyecto final — Curso de Desarrollo Web 2026
3
+
4
+ ---
5
+
6
+ ## Descripción
7
+
8
+ Morphos es una aplicación web de apoyo al diagnóstico veterinario. Detecta patrones clínicos en tiempo real a partir de valores de laboratorio usando un motor propio de JS puro con la opción de interpretarlos mediante un modelo de inteligencia artificial especializado en medicina (medGemma 1.5 4B it multimodal de Google Deep Mind).
9
+
10
+ Está orientada a caninos y felinos, con ajuste automático de rangos de referencia por especie, edad, raza y sexo.
11
+ Ataca una necesidad real del sector veterinario que actualmente no dispone de herramientas de este tipo que sean gratuitas y de fácil uso y que permitan obtener información complementaria relevante sobre sus pacientes en muy poco tiempo y sin exponer la data sensible a los LLM.
12
+
13
+ Funcionalidades principales:
14
+
15
+ ```text
16
+ Detección de patrones clínicos en tiempo real con motor nativo de JS
17
+ Interpretación con IA (HuggingFace o Ollama local)
18
+ Importación de resultados desde PDF
19
+ Análisis de citologías mediante imágenes
20
+ Búsqueda de literatura científica en PubMed
21
+ Sistema de autenticación con registro e inicio de sesión
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Objetivo del proyecto
27
+
28
+ Integrar los conocimientos del curso en una aplicación web completa que además sea útil y
29
+ cubra una necesidad de mercado:
30
+
31
+ ```text
32
+ HTML semántico y accesible
33
+ CSS personalizado (variables, grid, responsive)
34
+ JavaScript modular
35
+ Sin uso de frameworks
36
+ PHP como backend de API (proxy, autenticación, base de datos)
37
+ ```
38
+
39
+ Conceptos aplicados:
40
+
41
+ * Separación de responsabilidades por módulos
42
+ * Comunicación asíncrona con `fetch` (JSON y SSE)
43
+ * Sesiones PHP y autenticación con PDO
44
+ * Contraseñas hasheadas con `password_hash`
45
+ * Consultas preparadas para prevenir inyección SQL
46
+ * Detección de patrones mediante lógica clínica codificada
47
+
48
+ ---
49
+
50
+ ## Estructura del proyecto
51
+
52
+ ```text
53
+ /api
54
+ auth.php → login, registro y cierre de sesión (PDO + sesiones)
55
+ conexion.php → conexión a la base de datos MySQL
56
+ hf_proxy.php → proxy hacia HuggingFace Space (oculta la API key)
57
+ papers_proxy.php → consulta PubMed con caché de 30 minutos
58
+ setup.php → crea la base de datos y la tabla de usuarios
59
+ .env → variables de entorno (HF_API_KEY, DB_PORT)
60
+
61
+ /js
62
+ main.js → orquestación general, eventos y renderizado
63
+ analisis.js → motor de detección de patrones clínicos
64
+ ia.js → construcción del prompt y llamadas al modelo IA
65
+ ui.js → navegación por tabs, gestos, sincronización móvil
66
+ auth.js → modal de autenticación y validación en tiempo real
67
+ papers.js → búsqueda y paginación de literatura científica
68
+ pdf-parser.js → extracción de valores desde PDF en el navegador
69
+
70
+ /css
71
+ styles.css → estilos completos (tema claro/oscuro, grid, mobile)
72
+
73
+ /data
74
+ valores_referencia.json → rangos de referencia por especie y analito
75
+ alteraciones.json → descripciones clínicas de los patrones
76
+
77
+ /assets
78
+ /fonts → Inter y JetBrains Mono (carga local)
79
+ /icons → iconos SVG de la interfaz
80
+ /lib/pdfjs → librería PDF.js en local
81
+
82
+ index.html → SPA principal
83
+ .htaccess → compresión, caché y protección de archivos sensibles
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Flujo de la aplicación
89
+
90
+ ```text
91
+ [ Formulario de valores ]
92
+ |
93
+ v
94
+ analisis.js
95
+ (deteccion de patrones en tiempo real, sin servidor)
96
+ |
97
+ v
98
+ [ UI: campos coloreados + tarjetas de patron ]
99
+ |
100
+ v
101
+ Usuario pulsa "Analisis IA"
102
+ |
103
+ v
104
+ ia.js (construye el prompt con los hallazgos)
105
+ |
106
+ ┌─┴──────────────┐
107
+ v v
108
+ HuggingFace Ollama local
109
+ (hf_proxy.php) (/v1/chat/completions)
110
+ | |
111
+ └────────┬────────┘
112
+ v
113
+ [ Interpretacion en pantalla ]
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Instalacion
119
+
120
+ ### 1. Requisitos
121
+
122
+ * XAMPP (Apache + PHP 8.1+ + MySQL)
123
+ * El proyecto ubicado en `htdocs/morphos_proyecto_final/`
124
+
125
+ ### 2. Variables de entorno
126
+
127
+ Crear el archivo `api/.env`:
128
+
129
+ ```text
130
+ HF_API_KEY=tu_clave_de_huggingface
131
+ DB_PORT=3306
132
+ ```
133
+
134
+ ### 3. Base de datos
135
+
136
+ Acceder en el navegador a:
137
+
138
+ ```text
139
+ http://localhost/morphos_proyecto_final/api/setup.php
140
+ ```
141
+
142
+ Esto crea la base de datos `morphos_db` y la tabla `usuarios`. El archivo puede eliminarse tras ejecutarse.
143
+
144
+ ### 4. Iniciar la aplicacion
145
+
146
+ Iniciar Apache y MySQL desde el panel de XAMPP y abrir:
147
+
148
+ ```text
149
+ http://localhost/morphos_proyecto_final/
150
+ ```
151
+
152
+ O bien con el servidor integrado de PHP desde la raiz del proyecto:
153
+
154
+ ```bash
155
+ php -S localhost:8000
156
+ ```
157
+
158
  ---
159
+
160
+ ## Backend de IA
161
+
162
+ El modelo de IA se configura desde la propia interfaz. La seleccion se guarda en `localStorage`.
163
+
164
+ | Opcion | Descripcion |
165
+ |---|---|
166
+ | HuggingFace (por defecto) | Llama al Space `blackmistcode-morphos-medgemma` a traves del proxy PHP |
167
+ | Local (Ollama) | Llama directamente a `http://localhost:11434` con `medgemma1.5:latest` |
168
+
169
+ Para usar Ollama, debe estar ejecutandose con `ollama serve` y el modelo descargado.
170
+
171
  ---
172
 
173
+ ## Motor de deteccion de patrones
174
+
175
+ `analisis.js` compara cada valor ingresado contra los rangos de referencia del JSON, ajustados dinamicamente segun:
176
+
177
+ * **Especie**: canino / felino
178
+ * **Edad**: cachorro, adulto, senior, geriatrico
179
+ * **Raza**: galgo/whippet (RBC y plaquetas), Shiba/Akita (RBC)
180
+ * **Sexo**: felinos machos tienen mayor tolerancia a creatinina
181
+
182
+ La gravedad se calcula como la desviacion relativa al ancho del rango de referencia. Con los hallazgos se identifican mas de 50 patrones clinicos (anemias, hepatopatias, nefropatia, alteraciones endocrinas, electrolitos, entre otros).
183
+
184
+ ---
185
+
186
+ ## Seguridad aplicada
187
+
188
+ * Consultas SQL con sentencias preparadas (sin interpolacion directa)
189
+ * Contrasenas hasheadas con `password_hash` / `password_verify`
190
+ * API key de HuggingFace protegida en servidor, nunca expuesta al cliente
191
+ * Archivos `.env` y `setup.php` bloqueados por `.htaccess`
192
+ * Datos externos de APIs sanitizados con `textContent` antes de insertarse en el DOM
193
+ * Sin uso de `eval`, `document.write` ni `innerHTML` con datos externos
194
+
195
+ ---
196
+
197
+ ## Conceptos del curso aplicados
198
+
199
+ * HTML5 semantico
200
+ * CSS personalizado: variables, fuentes fluidas, grid, flexbox, media queries, temas claro/oscuro
201
+ * JavaScript: ES Modules, `fetch`, `async/await`, eventos, DOM API
202
+ * PHP: sesiones, PDO, proxy HTTP con cURL, lectura de `.env`, caché en disco
203
+ * MariaDB: creacion de tablas, consultas con parametros, indices unicos
204
+
205
+ ---
206
+
207
+ ## Mejoras futuras
208
+ * Implementación de dashboard de administrador
209
+ * Desarrollo de extensión de navegador para captar datos del DOM de PIMS y obtener los datos de los analisis de los pacientes con intervención mínima del usuario
210
+ * Desarrollo de mobile app dedicada
211
+ * Integración con PIMS más utilizados en veterinaria
212
+ * Rankeo de papers basado en confiabilidad y relevancia
213
+ * Creación de Dataset específico para citologías de animales
214
+ * Hosting del modelo en VPS serverless para finetuning y menor latencia
215
+ * Ampliación de la base de alteraciones
216
+ * Parseo con OCR de fotografías de analíticas
217
+ * Incluir resultados de gasometría, coprologías, informes de histopatologías y tiempos de coagulación
218
+
219
+ ## Retos
220
+ * Por la diversidad de unidades de medición que utilizan los diferentes fabricantes de equipos de laboratorio se incorporó una detección de unidades para su conversión y normalización
221
+ * El modelado del output de la I.A requirió muchísimas iteraciones de formateo del prompt y harness
222
+ * Inicialmente quería usar proveedores de inferencia gratuita de medGemma (como featherless AI) pero fallaban continuamente, por eso decidí optar por hostear al modelo en Zero GPU de HF con la subscripción pro para la prueba de concepto
223
+ * Incluir las librería de parseo de pdf y las fuentes en el directorio del proyecto con la intención de reducir dependencias externas estaba generando problemas con las métricas de velocidad de lighthouse que no lograba solucionar. Claude planteó implementación de caché en htacesss y pre carga de las fuentes, lo cual llevó la puntuación de 60 a 90/100 sin mayores cambios estructurales
224
+ * Lograr una interfaz limpia y entendible requirío de muchos intentos hasta lograr un flujo de trabajo intuitivo y accsesible con la mínima friccion posible para los usuarios
225
+ * La API de PubMed sólo admite input en inglés, así que implementó un objeto con traducciones de los patrones clínicos más comúnes para poder realizar las peticiones
226
+
227
+ ## Notas
228
+ * `api/setup.php` puede eliminarse una vez creada la base de datos
229
+ * El parser de PDF funciona completamente en el navegador (sin subida al servidor) para evitar enviar información privada al modelo de IA.
230
+ * La busqueda de literatura filtra los patrones detectados, los traduce al ingles y consulta PubMed via `esearch` + `esummary`
SKILL.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: hf-cli
3
+ description: "Hugging Face Hub CLI (`hf`) for downloading, uploading, and managing models, datasets, spaces, buckets, repos, papers, jobs, and more on the Hugging Face Hub. Use when: handling authentication; managing local cache; managing Hugging Face Buckets; running or scheduling jobs on Hugging Face infrastructure; managing Hugging Face repos; discussions and pull requests; browsing models, datasets and spaces; reading, searching, or browsing academic papers; managing collections; querying datasets; configuring spaces; setting up webhooks; or deploying and managing HF Inference Endpoints. Make sure to use this skill whenever the user mentions 'hf', 'huggingface', 'Hugging Face', 'huggingface-cli', or 'hugging face cli', or wants to do anything related to the Hugging Face ecosystem and to AI and ML in general. Also use for cloud storage needs like training checkpoints, data pipelines, or agent traces. Use even if the user doesn't explicitly ask for a CLI command. Replaces the deprecated `huggingface-cli`."
4
+ ---
5
+
6
+ Install: `curl -LsSf https://hf.co/cli/install.sh | bash -s`.
7
+
8
+ The Hugging Face Hub CLI tool `hf` is available. IMPORTANT: The `hf` command replaces the deprecated `huggingface-cli` command.
9
+
10
+ Use `hf --help` to view available functions. Note that auth commands are now all under `hf auth` e.g. `hf auth whoami`.
11
+
12
+ Generated with `huggingface_hub v1.13.0`. Run `hf skills add --force` to regenerate.
13
+
14
+ ## Commands
15
+
16
+ - `hf download REPO_ID` — Download files from the Hub. `[--type CHOICE --revision TEXT --include TEXT --exclude TEXT --cache-dir TEXT --local-dir TEXT --force-download --dry-run --max-workers INTEGER --format CHOICE]`
17
+ - `hf env` — Print information about the environment. `[--format CHOICE]`
18
+ - `hf sync` — Sync files between local directory and a bucket. `[--delete --ignore-times --ignore-sizes --plan TEXT --apply TEXT --dry-run --include TEXT --exclude TEXT --filter-from TEXT --existing --ignore-existing --verbose --format CHOICE]`
19
+ - `hf update` — Update the `hf` CLI to the latest version. `[--format CHOICE]`
20
+ - `hf upload REPO_ID` — Upload a file or a folder to the Hub. Recommended for single-commit uploads. `[--type CHOICE --revision TEXT --private --include TEXT --exclude TEXT --delete TEXT --commit-message TEXT --commit-description TEXT --create-pr --every FLOAT --format CHOICE]`
21
+ - `hf upload-large-folder REPO_ID LOCAL_PATH` — Upload a large folder to the Hub. Recommended for resumable uploads. `[--type CHOICE --revision TEXT --private --include TEXT --exclude TEXT --num-workers INTEGER --no-report --no-bars --format CHOICE]`
22
+ - `hf version` — Print information about the hf version. `[--format CHOICE]`
23
+
24
+ ### `hf auth` — Manage authentication (login, logout, etc.).
25
+
26
+ - `hf auth list` — List all stored access tokens. `[--format CHOICE]`
27
+ - `hf auth login` — Login using a token from huggingface.co/settings/tokens. `[--add-to-git-credential --force --format CHOICE]`
28
+ - `hf auth logout` — Logout from a specific token. `[--token-name TEXT --format CHOICE]`
29
+ - `hf auth switch` — Switch between access tokens. `[--token-name TEXT --add-to-git-credential --format CHOICE]`
30
+ - `hf auth token` — Print the current access token to stdout. `[--format CHOICE]`
31
+ - `hf auth whoami` — Find out which huggingface.co account you are logged in as. `[--format CHOICE]`
32
+
33
+ ### `hf buckets` — Commands to interact with buckets.
34
+
35
+ - `hf buckets cp SRC` — Copy files to or from buckets. `[--format CHOICE]`
36
+ - `hf buckets create BUCKET_ID` — Create a new bucket. `[--private --exist-ok --format CHOICE]`
37
+ - `hf buckets delete BUCKET_ID` — Delete a bucket. `[--yes --missing-ok --format CHOICE]`
38
+ - `hf buckets info BUCKET_ID` — Get info about a bucket. `[--format CHOICE]`
39
+ - `hf buckets list` — List buckets or files in a bucket. `[--human-readable --tree --recursive --search TEXT --format CHOICE]`
40
+ - `hf buckets move FROM_ID TO_ID` — Move (rename) a bucket to a new name or namespace. `[--format CHOICE]`
41
+ - `hf buckets remove ARGUMENT` — Remove files from a bucket. `[--recursive --yes --dry-run --include TEXT --exclude TEXT --format CHOICE]`
42
+ - `hf buckets sync` — Sync files between local directory and a bucket. `[--delete --ignore-times --ignore-sizes --plan TEXT --apply TEXT --dry-run --include TEXT --exclude TEXT --filter-from TEXT --existing --ignore-existing --verbose --format CHOICE]`
43
+
44
+ ### `hf cache` — Manage local cache directory.
45
+
46
+ - `hf cache list` — List cached repositories or revisions. `[--cache-dir TEXT --revisions --filter TEXT --sort CHOICE --limit INTEGER --format CHOICE]`
47
+ - `hf cache prune` — Remove detached revisions from the cache. `[--cache-dir TEXT --yes --dry-run --format CHOICE]`
48
+ - `hf cache rm TARGETS` — Remove cached repositories or revisions. `[--cache-dir TEXT --yes --dry-run --format CHOICE]`
49
+ - `hf cache verify REPO_ID` — Verify checksums for a single repo revision from cache or a local directory. `[--type CHOICE --revision TEXT --cache-dir TEXT --local-dir TEXT --fail-on-missing-files --fail-on-extra-files --format CHOICE]`
50
+
51
+ ### `hf collections` — Interact with collections on the Hub.
52
+
53
+ - `hf collections add-item COLLECTION_SLUG ITEM_ID ITEM_TYPE` — Add an item to a collection. `[--note TEXT --exists-ok --format CHOICE]`
54
+ - `hf collections create TITLE` — Create a new collection on the Hub. `[--namespace TEXT --description TEXT --private --exists-ok --format CHOICE]`
55
+ - `hf collections delete COLLECTION_SLUG` — Delete a collection from the Hub. `[--missing-ok --format CHOICE]`
56
+ - `hf collections delete-item COLLECTION_SLUG ITEM_OBJECT_ID` — Delete an item from a collection. `[--missing-ok --format CHOICE]`
57
+ - `hf collections info COLLECTION_SLUG` — Get info about a collection on the Hub. `[--format CHOICE]`
58
+ - `hf collections list` — List collections on the Hub. `[--owner TEXT --item TEXT --sort CHOICE --limit INTEGER --format CHOICE]`
59
+ - `hf collections update COLLECTION_SLUG` — Update a collection's metadata on the Hub. `[--title TEXT --description TEXT --position INTEGER --private --theme TEXT --format CHOICE]`
60
+ - `hf collections update-item COLLECTION_SLUG ITEM_OBJECT_ID` — Update an item in a collection. `[--note TEXT --position INTEGER --format CHOICE]`
61
+
62
+ ### `hf datasets` — Interact with datasets on the Hub.
63
+
64
+ - `hf datasets card DATASET_ID` — Get the dataset card (README) for a dataset on the Hub. `[--metadata --text --format CHOICE]`
65
+ - `hf datasets info DATASET_ID` — Get info about a dataset on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
66
+ - `hf datasets leaderboard DATASET_ID` — List model scores from a dataset leaderboard. This command helps find the best models for a task or compare models by benchmark scores. `[--limit INTEGER --format CHOICE]`
67
+ - `hf datasets list` — List datasets on the Hub, or files in a dataset repo. `[--search TEXT --author TEXT --filter TEXT --sort CHOICE --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format CHOICE]`
68
+ - `hf datasets parquet DATASET_ID` — List parquet file URLs available for a dataset. `[--subset TEXT --split TEXT --format CHOICE]`
69
+ - `hf datasets sql SQL` — Execute a raw SQL query with DuckDB against dataset parquet URLs. `[--format CHOICE]`
70
+
71
+ ### `hf discussions` — Manage discussions and pull requests on the Hub.
72
+
73
+ - `hf discussions close REPO_ID NUM` — Close a discussion or pull request. `[--comment TEXT --yes --type CHOICE --format CHOICE]`
74
+ - `hf discussions comment REPO_ID NUM` — Comment on a discussion or pull request. `[--body TEXT --body-file PATH --type CHOICE --format CHOICE]`
75
+ - `hf discussions create REPO_ID --title TEXT` — Create a new discussion or pull request on a repo. `[--body TEXT --body-file PATH --pull-request --type CHOICE --format CHOICE]`
76
+ - `hf discussions diff REPO_ID NUM` — Show the diff of a pull request. `[--type CHOICE --format CHOICE]`
77
+ - `hf discussions info REPO_ID NUM` — Get info about a discussion or pull request. `[--type CHOICE --format CHOICE]`
78
+ - `hf discussions list REPO_ID` — List discussions and pull requests on a repo. `[--status CHOICE --kind CHOICE --author TEXT --limit INTEGER --type CHOICE --format CHOICE]`
79
+ - `hf discussions merge REPO_ID NUM` — Merge a pull request. `[--comment TEXT --yes --type CHOICE --format CHOICE]`
80
+ - `hf discussions rename REPO_ID NUM NEW_TITLE` — Rename a discussion or pull request. `[--type CHOICE --format CHOICE]`
81
+ - `hf discussions reopen REPO_ID NUM` — Reopen a closed discussion or pull request. `[--comment TEXT --yes --type CHOICE --format CHOICE]`
82
+
83
+ ### `hf endpoints` — Manage Hugging Face Inference Endpoints.
84
+
85
+ - `hf endpoints catalog deploy --repo TEXT` — Deploy an Inference Endpoint from the Model Catalog. `[--name TEXT --accelerator TEXT --namespace TEXT --format CHOICE]`
86
+ - `hf endpoints catalog list` — List available Catalog models. `[--format CHOICE]`
87
+ - `hf endpoints delete NAME` — Delete an Inference Endpoint permanently. `[--namespace TEXT --yes --format CHOICE]`
88
+ - `hf endpoints deploy NAME --repo TEXT --framework TEXT --accelerator TEXT --instance-size TEXT --instance-type TEXT --region TEXT --vendor TEXT` — Deploy an Inference Endpoint from a Hub repository. `[--namespace TEXT --task TEXT --min-replica INTEGER --max-replica INTEGER --scale-to-zero-timeout INTEGER --scaling-metric CHOICE --scaling-threshold FLOAT --format CHOICE]`
89
+ - `hf endpoints describe NAME` — Get information about an existing endpoint. `[--namespace TEXT --format CHOICE]`
90
+ - `hf endpoints list` — Lists all Inference Endpoints for the given namespace. `[--namespace TEXT --format CHOICE]`
91
+ - `hf endpoints pause NAME` — Pause an Inference Endpoint. `[--namespace TEXT --format CHOICE]`
92
+ - `hf endpoints resume NAME` — Resume an Inference Endpoint. `[--namespace TEXT --fail-if-already-running --format CHOICE]`
93
+ - `hf endpoints scale-to-zero NAME` — Scale an Inference Endpoint to zero. `[--namespace TEXT --format CHOICE]`
94
+ - `hf endpoints update NAME` — Update an existing endpoint. `[--namespace TEXT --repo TEXT --accelerator TEXT --instance-size TEXT --instance-type TEXT --framework TEXT --revision TEXT --task TEXT --min-replica INTEGER --max-replica INTEGER --scale-to-zero-timeout INTEGER --scaling-metric CHOICE --scaling-threshold FLOAT --format CHOICE]`
95
+
96
+ ### `hf extensions` — Manage hf CLI extensions.
97
+
98
+ - `hf extensions exec NAME` — Execute an installed extension.
99
+ - `hf extensions install REPO_ID` — Install an extension from a public GitHub repository. `[--force --format CHOICE]`
100
+ - `hf extensions list` — List installed extension commands. `[--format CHOICE]`
101
+ - `hf extensions remove NAME` — Remove an installed extension. `[--format CHOICE]`
102
+ - `hf extensions search` — Search extensions available on GitHub (tagged with 'hf-extension' topic). `[--format CHOICE]`
103
+
104
+ ### `hf jobs` — Run and manage Jobs on the Hub.
105
+
106
+ - `hf jobs cancel JOB_ID` — Cancel a Job `[--namespace TEXT --format CHOICE]`
107
+ - `hf jobs hardware` — List available hardware options for Jobs `[--format CHOICE]`
108
+ - `hf jobs inspect JOB_IDS` — Display detailed information on one or more Jobs `[--namespace TEXT --format CHOICE]`
109
+ - `hf jobs logs JOB_ID` — Fetch the logs of a Job. `[--follow --tail INTEGER --namespace TEXT --format CHOICE]`
110
+ - `hf jobs ps` — List Jobs. `[--all --namespace TEXT --filter TEXT --format TEXT --quiet]`
111
+ - `hf jobs run IMAGE COMMAND` — Run a Job. `[--env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --flavor CHOICE --timeout TEXT --detach --namespace TEXT]`
112
+ - `hf jobs scheduled delete SCHEDULED_JOB_ID` — Delete a scheduled Job. `[--namespace TEXT --format CHOICE]`
113
+ - `hf jobs scheduled inspect SCHEDULED_JOB_IDS` — Display detailed information on one or more scheduled Jobs `[--namespace TEXT --format CHOICE]`
114
+ - `hf jobs scheduled ps` — List scheduled Jobs `[--all --namespace TEXT --filter TEXT --format TEXT --quiet]`
115
+ - `hf jobs scheduled resume SCHEDULED_JOB_ID` — Resume (unpause) a scheduled Job. `[--namespace TEXT --format CHOICE]`
116
+ - `hf jobs scheduled run SCHEDULE IMAGE COMMAND` — Schedule a Job. `[--suspend --concurrency --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --flavor CHOICE --timeout TEXT --namespace TEXT]`
117
+ - `hf jobs scheduled suspend SCHEDULED_JOB_ID` — Suspend (pause) a scheduled Job. `[--namespace TEXT --format CHOICE]`
118
+ - `hf jobs scheduled uv run SCHEDULE SCRIPT` — Run a UV script (local file or URL) on HF infrastructure `[--suspend --concurrency --image TEXT --flavor CHOICE --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --timeout TEXT --namespace TEXT --with TEXT --python TEXT]`
119
+ - `hf jobs stats` — Fetch the resource usage statistics and metrics of Jobs `[--namespace TEXT --format CHOICE]`
120
+ - `hf jobs uv run SCRIPT` — Run a UV script (local file or URL) on HF infrastructure `[--image TEXT --flavor CHOICE --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --timeout TEXT --detach --namespace TEXT --with TEXT --python TEXT]`
121
+
122
+ ### `hf models` — Interact with models on the Hub.
123
+
124
+ - `hf models card MODEL_ID` — Get the model card (README) for a model on the Hub. `[--metadata --text --format CHOICE]`
125
+ - `hf models info MODEL_ID` — Get info about a model on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
126
+ - `hf models list` — List models on the Hub, or files in a model repo. `[--search TEXT --author TEXT --filter TEXT --num-parameters TEXT --sort CHOICE --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format CHOICE]`
127
+
128
+ ### `hf papers` — Interact with papers on the Hub.
129
+
130
+ - `hf papers info PAPER_ID` — Get info about a paper on the Hub. `[--format CHOICE]`
131
+ - `hf papers list` — List daily papers on the Hub. `[--date TEXT --week TEXT --month TEXT --submitter TEXT --sort CHOICE --limit INTEGER --format CHOICE]`
132
+ - `hf papers read PAPER_ID` — Read a paper as markdown. `[--format CHOICE]`
133
+ - `hf papers search QUERY` — Search papers on the Hub. `[--limit INTEGER --format CHOICE]`
134
+
135
+ ### `hf repos` — Manage repos on the Hub.
136
+
137
+ - `hf repos branch create REPO_ID BRANCH` — Create a new branch for a repo on the Hub. `[--revision TEXT --type CHOICE --exist-ok --format CHOICE]`
138
+ - `hf repos branch delete REPO_ID BRANCH` — Delete a branch from a repo on the Hub. `[--type CHOICE --format CHOICE]`
139
+ - `hf repos create REPO_ID` — Create a new repo on the Hub. `[--type CHOICE --space-sdk TEXT --private --public --protected --exist-ok --resource-group-id TEXT --flavor CHOICE --storage CHOICE --sleep-time INTEGER --secrets TEXT --secrets-file TEXT --env TEXT --env-file TEXT --volume TEXT --format CHOICE]`
140
+ - `hf repos delete REPO_ID` — Delete a repo from the Hub. This is an irreversible operation. `[--type CHOICE --missing-ok --yes --format CHOICE]`
141
+ - `hf repos delete-files REPO_ID PATTERNS` — Delete files from a repo on the Hub. `[--type CHOICE --revision TEXT --commit-message TEXT --commit-description TEXT --create-pr --format CHOICE]`
142
+ - `hf repos duplicate FROM_ID` — Duplicate a repo on the Hub (model, dataset, or Space). `[--type CHOICE --private --public --protected --exist-ok --flavor CHOICE --storage CHOICE --sleep-time INTEGER --secrets TEXT --secrets-file TEXT --env TEXT --env-file TEXT --volume TEXT --format CHOICE]`
143
+ - `hf repos move FROM_ID TO_ID` — Move a repository from a namespace to another namespace. `[--type CHOICE --format CHOICE]`
144
+ - `hf repos settings REPO_ID` — Update the settings of a repository. `[--gated CHOICE --private --public --protected --type CHOICE --format CHOICE]`
145
+ - `hf repos tag create REPO_ID TAG` — Create a tag for a repo. `[--message TEXT --revision TEXT --type CHOICE --format CHOICE]`
146
+ - `hf repos tag delete REPO_ID TAG` — Delete a tag for a repo. `[--yes --type CHOICE --format CHOICE]`
147
+ - `hf repos tag list REPO_ID` — List tags for a repo. `[--type CHOICE --format CHOICE]`
148
+
149
+ ### `hf skills` — Manage skills for AI assistants.
150
+
151
+ - `hf skills add` — Download a Hugging Face skill and install it for an AI assistant. `[--claude --global --dest PATH --force --format CHOICE]`
152
+ - `hf skills preview` — Print the generated `hf-cli` SKILL.md to stdout. `[--format CHOICE]`
153
+ - `hf skills upgrade` — Upgrade installed Hugging Face marketplace skills. `[--claude --global --dest PATH --format CHOICE]`
154
+
155
+ ### `hf spaces` — Interact with spaces on the Hub.
156
+
157
+ - `hf spaces card SPACE_ID` — Get the Space card (README) for a Space on the Hub. `[--metadata --text --format CHOICE]`
158
+ - `hf spaces dev-mode SPACE_ID` — Enable or disable dev mode on a Space. `[--stop --format CHOICE]`
159
+ - `hf spaces hardware` — List available hardware options for Spaces. `[--format CHOICE]`
160
+ - `hf spaces hot-reload SPACE_ID` — Hot-reload any Python file of a Space without a full rebuild + restart. `[--local-file PATH --skip-checks --skip-summary --format CHOICE]`
161
+ - `hf spaces info SPACE_ID` — Get info about a space on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
162
+ - `hf spaces list` — List spaces on the Hub, or files in a space repo. `[--search TEXT --author TEXT --filter TEXT --sort CHOICE --limit INTEGER --expand TEXT --human-readable --tree --recursive --revision TEXT --format CHOICE]`
163
+ - `hf spaces logs SPACE_ID` — Fetch the run or build logs of a Space. `[--build --follow --tail INTEGER --format CHOICE]`
164
+ - `hf spaces pause SPACE_ID` — Pause a Space. `[--format CHOICE]`
165
+ - `hf spaces restart SPACE_ID` — Restart a Space. `[--factory-reboot --format CHOICE]`
166
+ - `hf spaces search QUERY` — Search spaces on the Hub using semantic search. `[--filter TEXT --sdk TEXT --include-non-running --description --limit INTEGER --format CHOICE]`
167
+ - `hf spaces settings SPACE_ID` — Update the settings of a Space. `[--sleep-time INTEGER --hardware CHOICE --format CHOICE]`
168
+ - `hf spaces volumes delete SPACE_ID` — Remove all volumes from a Space. `[--yes --format CHOICE]`
169
+ - `hf spaces volumes list SPACE_ID` — List volumes mounted in a Space. `[--format CHOICE]`
170
+ - `hf spaces volumes set SPACE_ID` — Set (replace) volumes for a Space. `[--volume TEXT --format CHOICE]`
171
+
172
+ ### `hf webhooks` — Manage webhooks on the Hub.
173
+
174
+ - `hf webhooks create --watch TEXT` — Create a new webhook. `[--url TEXT --job-id TEXT --domain CHOICE --secret TEXT --format CHOICE]`
175
+ - `hf webhooks delete WEBHOOK_ID` — Delete a webhook permanently. `[--yes --format CHOICE]`
176
+ - `hf webhooks disable WEBHOOK_ID` — Disable an active webhook. `[--format CHOICE]`
177
+ - `hf webhooks enable WEBHOOK_ID` — Enable a disabled webhook. `[--format CHOICE]`
178
+ - `hf webhooks info WEBHOOK_ID` — Show full details for a single webhook. `[--format CHOICE]`
179
+ - `hf webhooks list` — List all webhooks for the current user. `[--format CHOICE]`
180
+ - `hf webhooks update WEBHOOK_ID` — Update an existing webhook. Only provided options are changed. `[--url TEXT --watch TEXT --domain CHOICE --secret TEXT --format CHOICE]`
181
+
182
+ ## Common options
183
+
184
+ - `--format` — Output format: `--format json` (or `--json`) or `--format table` (default).
185
+ - `-q / --quiet` — Print only IDs (one per line).
186
+ - `--revision` — Git revision id which can be a branch name, a tag, or a commit hash.
187
+ - `--token` — Use a User Access Token. Prefer setting `HF_TOKEN` env var instead of passing `--token`.
188
+ - `--type` — The type of repository (model, dataset, or space).
189
+
190
+ ## Mounting repos as local filesystems
191
+
192
+ To mount Hub repositories or buckets as local filesystems — no download, no copy, no waiting — use `hf-mount`. Files are fetched on demand. GitHub: https://github.com/huggingface/hf-mount
193
+
194
+ Install: `curl -fsSL https://raw.githubusercontent.com/huggingface/hf-mount/main/install.sh | sh`
195
+
196
+ Some command examples:
197
+ - `hf-mount start repo openai-community/gpt2 /tmp/gpt2` — mount a repo (read-only)
198
+ - `hf-mount start --hf-token $HF_TOKEN bucket myuser/my-bucket /tmp/data` — mount a bucket (read-write)
199
+ - `hf-mount status` / `hf-mount stop /tmp/data` — list or unmount
200
+
201
+ ## Tips
202
+
203
+ - Use `hf <command> --help` for full options, descriptions, usage, and real-world examples
204
+ - Authenticate with `HF_TOKEN` env var (recommended) or with `--token`
205
+ - Update the CLI with `hf update` (uses the correct command for the detected install method)
USO_DE_IA.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Uso de inteligencia artificial en el desarrollo de Morphos
2
+ ## Proyecto final — Curso de Desarrollo Web 2026
3
+
4
+ ---
5
+
6
+ ## Modelo utilizado
7
+
8
+ Se utilizó exclusivamente Claude Code (Sonnet 4.6) de Anthropic como asistente de desarrollo a lo largo de todo el proyecto.
9
+
10
+ ---
11
+
12
+ ## Enfoque: Spec Driven Development
13
+
14
+ El uso de IA no consistió en generar código y aceptarlo sin más. Se aplicó un enfoque de desarrollo guiado por especificaciones: en cada paso se definió primero el comportamiento esperado, funciones base, las restricciones de diseño y los criterios de aceptación, y el modelo generó propuestas dentro de ese marco. La arquitectura, la separación de módulos, las decisiones de estructura y el flujo de datos fueron definidos y controlados por mi parte en todo momento gracias a los criterios y conocimientos adquiridos durante el curso.
15
+
16
+ El modelo actuó como ejecutor de decisiones ya tomadas, no como tomador de decisiones.
17
+
18
+ ---
19
+
20
+ ## Literatura de referencia en patología clínica veterinaria
21
+
22
+ Para el motor de detección de patrones (`analisis.js`) y los datos de referencia (`valores_referencia.json`, `alteraciones.json`), se proporcionó al modelo literatura especializada en patología clínica veterinaria como contexto de generación.
23
+
24
+ Los rangos de referencia por especie, los ajustes por edad, raza y sexo, las descripciones clínicas de cada alteración, y la lógica de clasificación de gravedad fueron **validados gracias a mi formación y experiencia profesional en Medicina Veterinaria**, antes de ser incorporados al código. El modelo fue un medio para estructurar y codificar ese conocimiento, no la fuente del mismo.
25
+
26
+ Textos de referencia utilizados:
27
+ - Thrall, *Veterinary Hematology and Clinical Chemistry*, 3.ª ed. 2022
28
+ - Weiss, — *Schalm's Veterinary Hematology*, 7.ª ed. 2022
29
+ ---
30
+
31
+ ## Afinamiento del prompt y control de salidas del modelo de IA
32
+
33
+ Morphos utiliza medGemma como modelo de interpretación clínica. Durante el desarrollo se detectó que el modelo producía salidas con problemas recurrentes:
34
+
35
+ - Respuestas en inglés a pesar de instrucciones en español
36
+ - Tokens de control expuestos en la salida (`<start_of_turn>`, `<unused94>`, `<unused95>`)
37
+ - Bloques de LaTeX embebidos en la respuesta
38
+ - Párrafos repetidos en bucle al acercarse al límite de tokens
39
+ - Prefijos de rol visibles al inicio del texto
40
+
41
+ Se iteró sobre el prompt y se implementó una función de limpieza de salida (`limpiarRespuesta` en `ia.js`) que elimina estos artefactos antes de mostrar el resultado al usuario. El prompt final incluye restricciones explícitas de idioma, alcance clínico y formato de respuesta.
42
+
43
+ ---
44
+
45
+ ## Caché y optimización de rendimiento
46
+
47
+ Durante las auditorías de rendimiento con Lighthouse se identificó que la carga de fuentes tipográficas locales y librerías (PDF.js) afectaba negativamente las métricas. Se implementaron las siguientes mejoras, asistidas por el modelo:
48
+
49
+ - Directivas `preload` y `preconnect` en el `<head>` para recursos críticos
50
+ - Caché de larga duración para fuentes, CSS y JS en `.htaccess`
51
+ - Caché en disco de 30 minutos para las respuestas de la API de PubMed, evitando llamadas repetidas a la misma consulta
52
+ - Compresión gzip habilitada por tipo de contenido
53
+
54
+ ---
55
+
56
+ ## Auditoría final de código
57
+
58
+ Al concluir el desarrollo se realizó una auditoría asistida por IA con los siguientes objetivos:
59
+
60
+ **Código muerto**
61
+ - Identificación y eliminación de exportaciones sin consumidores y código que ya no era necesario o era experimental
62
+
63
+ **Seguridad**
64
+ - Protección de archivos sensibles (`.env`, `setup.php`) mediante `.htaccess`
65
+ - Sanitización de datos externos de APIs con `textContent` en lugar de `innerHTML`, eliminando el riesgo de XSS
66
+ - Validación de URLs externas antes de usarlas como atributos `href`
67
+ - Separación del nombre de usuario del SVG estático al actualizar el botón de sesión, evitando inyección de HTML desde la base de datos
68
+
69
+ ---
api/auth.php ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ session_start();
4
+ header('Content-Type: application/json; charset=utf-8');
5
+
6
+ // Verificar estado de sesión (GET)
7
+ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
8
+ echo json_encode([
9
+ 'autenticado' => isset($_SESSION['morphos_usuario']),
10
+ 'nombre' => $_SESSION['morphos_nombre'] ?? null,
11
+ ]);
12
+ exit;
13
+ }
14
+
15
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
16
+ http_response_code(405);
17
+ echo json_encode(['error' => 'Método no permitido.']);
18
+ exit;
19
+ }
20
+
21
+ require __DIR__ . '/conexion.php';
22
+
23
+ if (!$conexion) {
24
+ http_response_code(503);
25
+ echo json_encode(['error' => 'Error de conexión con la base de datos.']);
26
+ exit;
27
+ }
28
+
29
+ $cuerpo = json_decode(file_get_contents('php://input'), true) ?? [];
30
+ $accion = $cuerpo['accion'] ?? '';
31
+
32
+ switch ($accion) {
33
+
34
+ case 'login':
35
+ $email = trim($cuerpo['email'] ?? '');
36
+ $password = $cuerpo['password'] ?? '';
37
+
38
+ if (!$email || !$password) {
39
+ http_response_code(422);
40
+ echo json_encode(['error' => 'Email y contraseña son requeridos.']);
41
+ exit;
42
+ }
43
+
44
+ $stmt = $conexion->prepare("SELECT id, nombre, email, password FROM usuarios WHERE email = :email LIMIT 1");
45
+ $stmt->bindParam(':email', $email);
46
+ $stmt->execute();
47
+ $usuario = $stmt->fetch(PDO::FETCH_ASSOC);
48
+
49
+ if ($usuario && password_verify($password, $usuario['password'])) {
50
+ $_SESSION['morphos_usuario'] = $usuario['email'];
51
+ $_SESSION['morphos_nombre'] = $usuario['nombre'];
52
+ echo json_encode(['ok' => true, 'nombre' => $usuario['nombre']]);
53
+ } else {
54
+ http_response_code(401);
55
+ echo json_encode(['error' => 'Email o contraseña incorrectos.']);
56
+ }
57
+ break;
58
+
59
+ case 'registro':
60
+ $nombre = trim($cuerpo['nombre'] ?? '');
61
+ $apellido = trim($cuerpo['apellido'] ?? '');
62
+ $email = trim($cuerpo['email'] ?? '');
63
+ $password = $cuerpo['password'] ?? '';
64
+
65
+ if (!$nombre || !$apellido || !$email || !$password) {
66
+ http_response_code(422);
67
+ echo json_encode(['error' => 'Todos los campos son requeridos.']);
68
+ exit;
69
+ }
70
+
71
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
72
+ http_response_code(422);
73
+ echo json_encode(['error' => 'El email no es válido.']);
74
+ exit;
75
+ }
76
+
77
+ if (strlen($password) < 6) {
78
+ http_response_code(422);
79
+ echo json_encode(['error' => 'La contraseña debe tener al menos 6 caracteres.']);
80
+ exit;
81
+ }
82
+
83
+ $stmt = $conexion->prepare("SELECT id FROM usuarios WHERE email = :email LIMIT 1");
84
+ $stmt->bindParam(':email', $email);
85
+ $stmt->execute();
86
+ if ($stmt->fetch()) {
87
+ http_response_code(409);
88
+ echo json_encode(['error' => 'Ya existe una cuenta con ese email.']);
89
+ exit;
90
+ }
91
+
92
+ $hash = password_hash($password, PASSWORD_DEFAULT);
93
+ $stmt = $conexion->prepare(
94
+ "INSERT INTO usuarios (nombre, apellido, email, password) VALUES (:nombre, :apellido, :email, :password)"
95
+ );
96
+ $stmt->bindParam(':nombre', $nombre);
97
+ $stmt->bindParam(':apellido', $apellido);
98
+ $stmt->bindParam(':email', $email);
99
+ $stmt->bindParam(':password', $hash);
100
+ $stmt->execute();
101
+
102
+ $_SESSION['morphos_usuario'] = $email;
103
+ $_SESSION['morphos_nombre'] = $nombre;
104
+ echo json_encode(['ok' => true, 'nombre' => $nombre]);
105
+ break;
106
+
107
+ case 'logout':
108
+ session_destroy();
109
+ echo json_encode(['ok' => true]);
110
+ break;
111
+
112
+ default:
113
+ http_response_code(400);
114
+ echo json_encode(['error' => 'Acción no válida.']);
115
+ }
api/conexion.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // Try MySQL first (local XAMPP), fallback to SQLite (Docker / HF Spaces)
4
+ $dbHost = '127.0.0.1';
5
+ $dbUsuario = 'root';
6
+ $dbClave = '';
7
+ $dbNombre = 'morphos_db';
8
+ $dbPort = 3306;
9
+ $dbPath = __DIR__ . '/../data/morphos.db';
10
+
11
+ if (file_exists(__DIR__ . '/.env')) {
12
+ foreach (file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
13
+ if (str_starts_with($line, 'DB_PORT=')) $dbPort = (int) trim(substr($line, 8));
14
+ }
15
+ }
16
+
17
+ $conexion = null;
18
+
19
+ // Attempt MySQL only if not explicitly forced to SQLite
20
+ $useSqlite = getenv('DB_FORCE_SQLITE') === '1';
21
+
22
+ if (!$useSqlite) {
23
+ try {
24
+ $conexion = new PDO("mysql:host=$dbHost;port=$dbPort;dbname=$dbNombre;charset=utf8mb4", $dbUsuario, $dbClave);
25
+ $conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
26
+ } catch (PDOException $e) {
27
+ $conexion = null;
28
+ }
29
+ }
30
+
31
+ // Fallback to SQLite
32
+ if (!$conexion) {
33
+ try {
34
+ $conexion = new PDO("sqlite:$dbPath");
35
+ $conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
36
+
37
+ // Ensure users table exists (idempotent)
38
+ $conexion->exec("CREATE TABLE IF NOT EXISTS usuarios (
39
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
40
+ nombre TEXT NOT NULL,
41
+ apellido TEXT NOT NULL,
42
+ email TEXT NOT NULL UNIQUE,
43
+ password TEXT NOT NULL,
44
+ creado_en DATETIME DEFAULT CURRENT_TIMESTAMP
45
+ )");
46
+ } catch (PDOException $e) {
47
+ $conexion = null;
48
+ }
49
+ }
api/hf_proxy.php ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ header('Content-Type: application/json; charset=utf-8');
3
+
4
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit; }
5
+
6
+ $hfKey = $_ENV['HF_API_KEY'] ?? $_SERVER['HF_API_KEY'] ?? getenv('HF_API_KEY') ?? '';
7
+
8
+ if (!$hfKey && file_exists(__DIR__ . '/.env')) {
9
+ foreach (file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
10
+ if (str_starts_with($line, 'HF_API_KEY=')) { $hfKey = trim(substr($line, 11)); break; }
11
+ }
12
+ }
13
+
14
+ if (!$hfKey) { http_response_code(503); echo json_encode(['error' => 'Servicio no configurado.']); exit; }
15
+
16
+ $body = json_decode(file_get_contents('php://input'), true);
17
+ $SPACE = 'https://blackmistcode-morphos-medgemma.hf.space/gradio_api';
18
+ $auth = ['Content-Type: application/json', "Authorization: Bearer $hfKey"];
19
+
20
+ set_time_limit(120);
21
+
22
+ function hf_get(string $url, array $headers, ?string $post = null): array {
23
+ $ch = curl_init($url);
24
+ curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 120]);
25
+ if ($post !== null) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); }
26
+ return [curl_exec($ch), curl_getinfo($ch, CURLINFO_HTTP_CODE)];
27
+ }
28
+
29
+ // Upload a data URL to the Gradio /upload endpoint and return a FileData object,
30
+ // or fall back to the raw data URL format if the upload fails.
31
+ function uploadImagen(string $space, string $hfKey, string $dataUrl): ?array {
32
+ if (!preg_match('/^data:(image\/[\w+]+);base64,(.+)$/s', $dataUrl, $m)) return null;
33
+ $mimeType = $m[1];
34
+ $ext = explode('/', $mimeType)[1] ?? 'jpg';
35
+ $binary = base64_decode($m[2]);
36
+ if ($binary === false) return null;
37
+
38
+ $boundary = bin2hex(random_bytes(16));
39
+ $body = "--$boundary\r\n"
40
+ . "Content-Disposition: form-data; name=\"files\"; filename=\"image.$ext\"\r\n"
41
+ . "Content-Type: $mimeType\r\n\r\n"
42
+ . $binary
43
+ . "\r\n--$boundary--\r\n";
44
+
45
+ $ch = curl_init("$space/upload");
46
+ curl_setopt_array($ch, [
47
+ CURLOPT_RETURNTRANSFER => true,
48
+ CURLOPT_POST => true,
49
+ CURLOPT_POSTFIELDS => $body,
50
+ CURLOPT_HTTPHEADER => [
51
+ "Authorization: Bearer $hfKey",
52
+ "Content-Type: multipart/form-data; boundary=$boundary",
53
+ ],
54
+ CURLOPT_TIMEOUT => 60,
55
+ ]);
56
+ $result = curl_exec($ch);
57
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
58
+ curl_close($ch);
59
+
60
+ if ($code >= 400 || !$result) {
61
+ // Fall back: pass as data URL directly (older Gradio versions accept this)
62
+ return ['url' => $dataUrl, 'orig_name' => "image.$ext", 'mime_type' => $mimeType];
63
+ }
64
+
65
+ $files = json_decode($result, true);
66
+ $path = is_array($files) && isset($files[0]) ? $files[0] : null;
67
+ if (!$path) {
68
+ return ['url' => $dataUrl, 'orig_name' => "image.$ext", 'mime_type' => $mimeType];
69
+ }
70
+
71
+ return [
72
+ 'path' => $path,
73
+ 'url' => "$space/file=" . $path,
74
+ 'orig_name' => "image.$ext",
75
+ 'mime_type' => $mimeType,
76
+ ];
77
+ }
78
+
79
+ $rawImages = array_slice(array_values($body['images'] ?? []), 0, 4);
80
+ $data = [];
81
+ foreach ($rawImages as $img) {
82
+ $data[] = $img ? uploadImagen($SPACE, $hfKey, $img) : null;
83
+ }
84
+ while (count($data) < 4) $data[] = null;
85
+ $data[] = $body['prompt'] ?? '';
86
+
87
+ [$submitBody, $code] = hf_get("$SPACE/call/analyze", $auth, json_encode(['data' => $data]));
88
+
89
+ if ($code >= 400) { http_response_code(502); echo json_encode(['error' => "Error Space: HTTP $code"]); exit; }
90
+
91
+ $eventId = json_decode($submitBody, true)['event_id'] ?? null;
92
+ if (!$eventId) { http_response_code(502); echo json_encode(['error' => 'No se obtuvo event_id.']); exit; }
93
+
94
+ [$stream] = hf_get("$SPACE/call/analyze/$eventId", ["Authorization: Bearer $hfKey"]);
95
+
96
+ $result = $error = null; $lastEvent = '';
97
+ foreach (explode("\n", $stream) as $raw) {
98
+ $line = rtrim($raw, "\r");
99
+ if (str_starts_with($line, 'event:')) $lastEvent = trim(substr($line, 6));
100
+ elseif (str_starts_with($line, 'data:')) {
101
+ $parsed = json_decode(trim(substr($line, 5)), true);
102
+ if (in_array($lastEvent, ['complete', 'process_completed']))
103
+ $result = is_array($parsed) ? $parsed[0] : ($parsed['output'] ?? $parsed);
104
+ elseif ($lastEvent === 'error')
105
+ $error = $parsed['error'] ?? 'Error del modelo.';
106
+ }
107
+ }
108
+
109
+ if ($error) { http_response_code(503); echo json_encode(['error' => $error]); }
110
+ elseif ($result !== null) { echo json_encode(['text' => $result]); }
111
+ else { http_response_code(502); echo json_encode(['error' => 'Sin respuesta del modelo.']); }
api/papers_proxy.php ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ header('Content-Type: application/json');
3
+
4
+ $consulta = trim($_GET['query'] ?? '');
5
+ if ($consulta === '') {
6
+ http_response_code(400);
7
+ echo json_encode(['error' => 'query requerido']);
8
+ exit;
9
+ }
10
+
11
+ $dirCache = sys_get_temp_dir() . '/morphos_papers_cache';
12
+ if (!is_dir($dirCache)) mkdir($dirCache, 0700, true);
13
+
14
+ function leerCache(string $clave, int $ttl): string|false {
15
+ global $dirCache;
16
+ $archivo = $dirCache . '/' . md5($clave) . '.json';
17
+ if (file_exists($archivo) && (time() - filemtime($archivo)) < $ttl) {
18
+ return file_get_contents($archivo);
19
+ }
20
+ return false;
21
+ }
22
+
23
+ function escribirCache(string $clave, string $contenido): void {
24
+ global $dirCache;
25
+ file_put_contents($dirCache . '/' . md5($clave) . '.json', $contenido);
26
+ }
27
+
28
+ $claveCache = 'pm:' . $consulta;
29
+ $cached = leerCache($claveCache, 1800);
30
+ if ($cached) { echo $cached; exit; }
31
+
32
+ function fetchHttp(string $url, array $cabeceras, int $timeout = 15): array {
33
+ $ctx = stream_context_create(['http' => [
34
+ 'method' => 'GET',
35
+ 'header' => implode("\r\n", $cabeceras),
36
+ 'timeout' => $timeout,
37
+ 'ignore_errors' => true,
38
+ ]]);
39
+ $body = @file_get_contents($url, false, $ctx);
40
+ $codigo = 0;
41
+ foreach ($http_response_header ?? [] as $h) {
42
+ if (preg_match('#HTTP/\S+\s+(\d+)#', $h, $m)) $codigo = (int)$m[1];
43
+ }
44
+ return ['body' => $body, 'codigo' => $codigo];
45
+ }
46
+
47
+ $cabeceras = ['User-Agent: Morphos/1.0 (mailto:ceo@equipamed.net)', 'Accept: application/json'];
48
+
49
+ $urlBusqueda = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi'
50
+ . '?db=pubmed&retmode=json&retmax=100&term=' . urlencode($consulta);
51
+
52
+ $resp = fetchHttp($urlBusqueda, $cabeceras);
53
+ if ($resp['body'] === false || $resp['codigo'] >= 400) {
54
+ http_response_code(502);
55
+ echo json_encode(['error' => 'No se pudo contactar PubMed.']);
56
+ exit;
57
+ }
58
+
59
+ $busqueda = json_decode($resp['body'], true);
60
+ $ids = $busqueda['esearchresult']['idlist'] ?? [];
61
+
62
+ if (empty($ids)) {
63
+ $salida = json_encode(['total' => 0, 'data' => []]);
64
+ escribirCache($claveCache, $salida);
65
+ echo $salida;
66
+ exit;
67
+ }
68
+
69
+ $urlResumen = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi'
70
+ . '?db=pubmed&retmode=json&id=' . implode(',', $ids);
71
+
72
+ $resp2 = fetchHttp($urlResumen, $cabeceras);
73
+ if ($resp2['body'] === false || $resp2['codigo'] >= 400) {
74
+ http_response_code(502);
75
+ echo json_encode(['error' => 'No se pudo obtener los resultados de PubMed.']);
76
+ exit;
77
+ }
78
+
79
+ $resumen = json_decode($resp2['body'], true);
80
+ $resultado = $resumen['result'] ?? [];
81
+ $uids = $resultado['uids'] ?? $ids;
82
+
83
+ $papers = [];
84
+ foreach ($uids as $uid) {
85
+ $p = $resultado[$uid] ?? null;
86
+ if (!$p) continue;
87
+
88
+ $anio = '';
89
+ if (!empty($p['pubdate'])) { preg_match('/\d{4}/', $p['pubdate'], $m); $anio = $m[0] ?? ''; }
90
+
91
+ $doi = '';
92
+ foreach ($p['articleids'] ?? [] as $aid) {
93
+ if ($aid['idtype'] === 'doi') { $doi = $aid['value']; break; }
94
+ }
95
+
96
+ $papers[] = [
97
+ 'pmid' => $uid,
98
+ 'title' => $p['title'] ?? 'Sin título',
99
+ 'authors' => array_map(fn($a) => ['name' => $a['name']], $p['authors'] ?? []),
100
+ 'year' => $anio,
101
+ 'doi' => $doi,
102
+ 'journal' => $p['source'] ?? '',
103
+ ];
104
+ }
105
+
106
+ $salida = json_encode(['total' => count($papers), 'data' => $papers]);
107
+ escribirCache($claveCache, $salida);
108
+ echo $salida;
api/setup.php ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ header('Content-Type: text/html; charset=utf-8');
4
+
5
+ $dbHost = '127.0.0.1';
6
+ $dbUsuario = 'root';
7
+ $dbClave = '';
8
+ $dbNombre = 'morphos_db';
9
+ $dbPort = 3306;
10
+
11
+ foreach (file(__DIR__ . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
12
+ if (str_starts_with($line, 'DB_PORT=')) $dbPort = (int) trim(substr($line, 8));
13
+ }
14
+
15
+ try {
16
+ $conexion = new PDO("mysql:host=$dbHost;port=$dbPort;charset=utf8mb4", $dbUsuario, $dbClave);
17
+ $conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
18
+
19
+ $conexion->exec("CREATE DATABASE IF NOT EXISTS `$dbNombre`");
20
+ $conexion->exec("USE `$dbNombre`");
21
+ $conexion->exec("
22
+ CREATE TABLE IF NOT EXISTS usuarios (
23
+ id INT AUTO_INCREMENT PRIMARY KEY,
24
+ nombre VARCHAR(100) NOT NULL,
25
+ apellido VARCHAR(100) NOT NULL,
26
+ email VARCHAR(150) NOT NULL UNIQUE,
27
+ password VARCHAR(255) NOT NULL,
28
+ creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
29
+ )
30
+ ");
31
+
32
+ echo "<p style='font-family:sans-serif;color:green'>✓ Base de datos <strong>$dbNombre</strong> y tabla <strong>usuarios</strong> listas.</p>";
33
+ echo "<p style='font-family:sans-serif'><a href='../index.html'>← Volver a Morphos</a></p>";
34
+
35
+ } catch (PDOException $e) {
36
+ echo "<p style='font-family:sans-serif;color:red'>Error: " . htmlspecialchars($e->getMessage()) . "</p>";
37
+ }
assets/fonts/Inter/OFL.txt ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://openfontlicense.org
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
assets/fonts/Inter/README.txt ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Inter Variable Font
2
+ ===================
3
+
4
+ This download contains Inter as both variable fonts and static fonts.
5
+
6
+ Inter is a variable font with these axes:
7
+ opsz
8
+ wght
9
+
10
+ This means all the styles are contained in these files:
11
+ Inter-VariableFont_opsz,wght.ttf
12
+ Inter-Italic-VariableFont_opsz,wght.ttf
13
+
14
+ If your app fully supports variable fonts, you can now pick intermediate styles
15
+ that aren’t available as static fonts. Not all apps support variable fonts, and
16
+ in those cases you can use the static font files for Inter:
17
+ static/Inter_18pt-Thin.ttf
18
+ static/Inter_18pt-ExtraLight.ttf
19
+ static/Inter_18pt-Light.ttf
20
+ static/Inter_18pt-Regular.ttf
21
+ static/Inter_18pt-Medium.ttf
22
+ static/Inter_18pt-SemiBold.ttf
23
+ static/Inter_18pt-Bold.ttf
24
+ static/Inter_18pt-ExtraBold.ttf
25
+ static/Inter_18pt-Black.ttf
26
+ static/Inter_24pt-Thin.ttf
27
+ static/Inter_24pt-ExtraLight.ttf
28
+ static/Inter_24pt-Light.ttf
29
+ static/Inter_24pt-Regular.ttf
30
+ static/Inter_24pt-Medium.ttf
31
+ static/Inter_24pt-SemiBold.ttf
32
+ static/Inter_24pt-Bold.ttf
33
+ static/Inter_24pt-ExtraBold.ttf
34
+ static/Inter_24pt-Black.ttf
35
+ static/Inter_28pt-Thin.ttf
36
+ static/Inter_28pt-ExtraLight.ttf
37
+ static/Inter_28pt-Light.ttf
38
+ static/Inter_28pt-Regular.ttf
39
+ static/Inter_28pt-Medium.ttf
40
+ static/Inter_28pt-SemiBold.ttf
41
+ static/Inter_28pt-Bold.ttf
42
+ static/Inter_28pt-ExtraBold.ttf
43
+ static/Inter_28pt-Black.ttf
44
+ static/Inter_18pt-ThinItalic.ttf
45
+ static/Inter_18pt-ExtraLightItalic.ttf
46
+ static/Inter_18pt-LightItalic.ttf
47
+ static/Inter_18pt-Italic.ttf
48
+ static/Inter_18pt-MediumItalic.ttf
49
+ static/Inter_18pt-SemiBoldItalic.ttf
50
+ static/Inter_18pt-BoldItalic.ttf
51
+ static/Inter_18pt-ExtraBoldItalic.ttf
52
+ static/Inter_18pt-BlackItalic.ttf
53
+ static/Inter_24pt-ThinItalic.ttf
54
+ static/Inter_24pt-ExtraLightItalic.ttf
55
+ static/Inter_24pt-LightItalic.ttf
56
+ static/Inter_24pt-Italic.ttf
57
+ static/Inter_24pt-MediumItalic.ttf
58
+ static/Inter_24pt-SemiBoldItalic.ttf
59
+ static/Inter_24pt-BoldItalic.ttf
60
+ static/Inter_24pt-ExtraBoldItalic.ttf
61
+ static/Inter_24pt-BlackItalic.ttf
62
+ static/Inter_28pt-ThinItalic.ttf
63
+ static/Inter_28pt-ExtraLightItalic.ttf
64
+ static/Inter_28pt-LightItalic.ttf
65
+ static/Inter_28pt-Italic.ttf
66
+ static/Inter_28pt-MediumItalic.ttf
67
+ static/Inter_28pt-SemiBoldItalic.ttf
68
+ static/Inter_28pt-BoldItalic.ttf
69
+ static/Inter_28pt-ExtraBoldItalic.ttf
70
+ static/Inter_28pt-BlackItalic.ttf
71
+
72
+ Get started
73
+ -----------
74
+
75
+ 1. Install the font files you want to use
76
+
77
+ 2. Use your app's font picker to view the font family and all the
78
+ available styles
79
+
80
+ Learn more about variable fonts
81
+ -------------------------------
82
+
83
+ https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
84
+ https://variablefonts.typenetwork.com
85
+ https://medium.com/variable-fonts
86
+
87
+ In desktop apps
88
+
89
+ https://theblog.adobe.com/can-variable-fonts-illustrator-cc
90
+ https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
91
+
92
+ Online
93
+
94
+ https://developers.google.com/fonts/docs/getting_started
95
+ https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
96
+ https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
97
+
98
+ Installing fonts
99
+
100
+ MacOS: https://support.apple.com/en-us/HT201749
101
+ Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
102
+ Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
103
+
104
+ Android Apps
105
+
106
+ https://developers.google.com/fonts/docs/android
107
+ https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
108
+
109
+ License
110
+ -------
111
+ Please read the full license text (OFL.txt) to understand the permissions,
112
+ restrictions and requirements for usage, redistribution, and modification.
113
+
114
+ You can use them in your products & projects – print or digital,
115
+ commercial or otherwise.
116
+
117
+ This isn't legal advice, please consider consulting a lawyer and see the full
118
+ license for all details.
assets/fonts/JetBrains_Mono/OFL.txt ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://openfontlicense.org
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
assets/fonts/JetBrains_Mono/README.txt ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ JetBrains Mono Variable Font
2
+ ============================
3
+
4
+ This download contains JetBrains Mono as both variable fonts and static fonts.
5
+
6
+ JetBrains Mono is a variable font with this axis:
7
+ wght
8
+
9
+ This means all the styles are contained in these files:
10
+ JetBrainsMono-VariableFont_wght.ttf
11
+ JetBrainsMono-Italic-VariableFont_wght.ttf
12
+
13
+ If your app fully supports variable fonts, you can now pick intermediate styles
14
+ that aren’t available as static fonts. Not all apps support variable fonts, and
15
+ in those cases you can use the static font files for JetBrains Mono:
16
+ static/JetBrainsMono-Thin.ttf
17
+ static/JetBrainsMono-ExtraLight.ttf
18
+ static/JetBrainsMono-Light.ttf
19
+ static/JetBrainsMono-Regular.ttf
20
+ static/JetBrainsMono-Medium.ttf
21
+ static/JetBrainsMono-SemiBold.ttf
22
+ static/JetBrainsMono-Bold.ttf
23
+ static/JetBrainsMono-ExtraBold.ttf
24
+ static/JetBrainsMono-ThinItalic.ttf
25
+ static/JetBrainsMono-ExtraLightItalic.ttf
26
+ static/JetBrainsMono-LightItalic.ttf
27
+ static/JetBrainsMono-Italic.ttf
28
+ static/JetBrainsMono-MediumItalic.ttf
29
+ static/JetBrainsMono-SemiBoldItalic.ttf
30
+ static/JetBrainsMono-BoldItalic.ttf
31
+ static/JetBrainsMono-ExtraBoldItalic.ttf
32
+
33
+ Get started
34
+ -----------
35
+
36
+ 1. Install the font files you want to use
37
+
38
+ 2. Use your app's font picker to view the font family and all the
39
+ available styles
40
+
41
+ Learn more about variable fonts
42
+ -------------------------------
43
+
44
+ https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
45
+ https://variablefonts.typenetwork.com
46
+ https://medium.com/variable-fonts
47
+
48
+ In desktop apps
49
+
50
+ https://theblog.adobe.com/can-variable-fonts-illustrator-cc
51
+ https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
52
+
53
+ Online
54
+
55
+ https://developers.google.com/fonts/docs/getting_started
56
+ https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
57
+ https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
58
+
59
+ Installing fonts
60
+
61
+ MacOS: https://support.apple.com/en-us/HT201749
62
+ Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
63
+ Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
64
+
65
+ Android Apps
66
+
67
+ https://developers.google.com/fonts/docs/android
68
+ https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
69
+
70
+ License
71
+ -------
72
+ Please read the full license text (OFL.txt) to understand the permissions,
73
+ restrictions and requirements for usage, redistribution, and modification.
74
+
75
+ You can use them in your products & projects – print or digital,
76
+ commercial or otherwise.
77
+
78
+ This isn't legal advice, please consider consulting a lawyer and see the full
79
+ license for all details.
assets/icons/1.svg ADDED
assets/icons/2.svg ADDED
assets/icons/3.svg ADDED
assets/icons/4.svg ADDED
assets/icons/5.svg ADDED
assets/icons/adjuntar.svg ADDED
assets/icons/anterior.svg ADDED
assets/icons/busqueda.svg ADDED
assets/icons/camara.svg ADDED
assets/icons/capturas.svg ADDED
assets/icons/citologias.svg ADDED
assets/icons/colapsar.svg ADDED
assets/icons/dark.svg ADDED
assets/icons/examenes.svg ADDED
assets/icons/expandir.svg ADDED
assets/icons/favicon.ico ADDED
assets/icons/flujo.svg ADDED
assets/icons/intepretacion.svg ADDED
assets/icons/light.svg ADDED
assets/icons/login.svg ADDED
assets/icons/logo.svg ADDED
assets/icons/logout.svg ADDED
assets/icons/mail.svg ADDED
assets/icons/paciente.svg ADDED
assets/icons/papelera.svg ADDED
assets/icons/remove.svg ADDED
assets/icons/siguiente.svg ADDED
assets/lib/pdfjs/pdf.min.js ADDED
The diff for this file is too large to render. See raw diff
 
assets/lib/pdfjs/pdf.worker.min.js ADDED
The diff for this file is too large to render. See raw diff
 
css/styles.css ADDED
@@ -0,0 +1,2607 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @charset "utf-8";
2
+
3
+ @font-face {
4
+ font-family: 'Inter';
5
+ src: url('../assets/fonts/Inter/Inter-VariableFont_opsz,wght.ttf') format('truetype');
6
+ font-weight: 100 900;
7
+ font-style: normal;
8
+ font-display: swap;
9
+ }
10
+
11
+ @font-face {
12
+ font-family: 'Inter';
13
+ src: url('../assets/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf') format('truetype');
14
+ font-weight: 100 900;
15
+ font-style: italic;
16
+ font-display: swap;
17
+ }
18
+
19
+ @font-face {
20
+ font-family: 'JetBrains Mono';
21
+ src: url('../assets/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf') format('truetype');
22
+ font-weight: 100 800;
23
+ font-style: normal;
24
+ font-display: swap;
25
+ }
26
+
27
+ :root {
28
+ --surface-page: #E8EEF5;
29
+ --surface-1: #FFFFFF;
30
+ --surface-header: #0A4F45;
31
+ --header-text: #FFFFFF;
32
+ --header-text-muted: rgba(255, 255, 255, 0.75);
33
+ --color-placeholder: #747474;
34
+ --header-text-action: rgba(255, 255, 255, 0.85);
35
+ --header-text-hover: #000000;
36
+ --header-input-bg: rgba(255, 255, 255, 0.12);
37
+ --header-input-border: rgba(255, 255, 255, 0.25);
38
+ --header-input-border-focus: rgba(255, 255, 255, 0.6);
39
+ --header-focus-ring: rgba(255, 255, 255, 0.1);
40
+ --surface-2: #E3EAF2;
41
+ --surface-3: #D6E0EB;
42
+ --surface-input: #FFFFFF;
43
+
44
+ --text-1: #0F1923;
45
+ --text-2: #2D3E50;
46
+ --text-3: #3D5470;
47
+ --text-4: #536B7A;
48
+ --text-accent: #0B6158;
49
+ --text-on-accent: #FFFFFF;
50
+
51
+ --border-1: #D0DAE4;
52
+ --border-2: #B8C6D4;
53
+ --border-focus: var(--accent);
54
+
55
+ --accent: #0F7A6B;
56
+ --accent-hover: #0B6158;
57
+ --accent-dim: #8DCCBE;
58
+ --focus-ring: rgba(15, 122, 107, 0.15);
59
+
60
+ --alto: #B8372C;
61
+ --alto-bg: rgba(184, 55, 44, 0.10);
62
+ --alto-border: rgba(184, 55, 44, 0.35);
63
+ --alto-strong: #8C271E;
64
+
65
+ --bajo: #2F6FB5;
66
+ --bajo-bg: rgba(47, 111, 181, 0.10);
67
+ --bajo-border: rgba(47, 111, 181, 0.35);
68
+ --bajo-strong: #21548D;
69
+
70
+ --urgent: #A21E1E;
71
+ --monitor: #B5791A;
72
+ --monitor-bg: rgba(181, 121, 26, 0.12);
73
+
74
+ --radius-sm: 4px;
75
+ --radius-md: 8px;
76
+
77
+ --space-1: 4px;
78
+ --space-2: 8px;
79
+ --space-3: 12px;
80
+ --space-4: 16px;
81
+ --space-5: 20px;
82
+ --space-6: 24px;
83
+
84
+ --dur-fast: 120ms;
85
+ --dur-base: 180ms;
86
+
87
+ --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
88
+ --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
89
+ --fs-xs: clamp(11px, 10.7px + 0.09vw, 12px);
90
+ --fs-sm: clamp(13px, 12.6px + 0.09vw, 14px);
91
+ --fs-ui: clamp(14px, 13.6px + 0.18vw, 16px);
92
+ --fs-base: clamp(16px, 15.7px + 0.09vw, 17px);
93
+ --fs-xl: clamp(22px, 21.3px + 0.18vw, 1.5rem);
94
+ --lh-snug: 1.4;
95
+ --lh-normal: 1.5;
96
+ --tracking-wide: 1px;
97
+ --tracking-wider: 0.1em;
98
+ --tracking-widest: 4px;
99
+ }
100
+
101
+ :root[data-theme="dark"] {
102
+ --surface-page: #0E1011;
103
+ --surface-header: var(--surface-1);
104
+ --header-text: var(--text-1);
105
+ --header-text-muted: var(--text-3);
106
+ --header-text-action: var(--text-2);
107
+ --header-text-hover: var(--header-text);
108
+ --header-input-bg: var(--surface-input);
109
+ --header-input-border: var(--border-1);
110
+ --header-input-border-focus: var(--border-focus);
111
+ --header-focus-ring: var(--focus-ring);
112
+ --surface-1: #16191B;
113
+ --surface-2: #1E2224;
114
+ --surface-3: #262B2E;
115
+ --surface-input: #121517;
116
+
117
+ --text-1: #FFFFFF;
118
+ --text-2: #E0E2DF;
119
+ --text-3: #C2C6C4;
120
+ --text-4: #B0B5B3;
121
+ --text-accent: #5ECDB8;
122
+ --text-on-accent: #062520;
123
+
124
+ --border-1: #2A2E31;
125
+ --border-2: #363B3E;
126
+ --border-focus: #5ECDB8;
127
+
128
+ --accent: #5ECDB8;
129
+ --accent-hover: #7BDAC7;
130
+ --accent-dim: #2A5A52;
131
+ --focus-ring: rgba(94, 205, 184, 0.15);
132
+
133
+ --alto: #F08478;
134
+ --alto-bg: rgba(240, 132, 120, 0.14);
135
+ --alto-border: rgba(240, 132, 120, 0.32);
136
+ --alto-strong: #F5A59B;
137
+
138
+ --bajo: #7EB8F0;
139
+ --bajo-bg: rgba(126, 184, 240, 0.14);
140
+ --bajo-border: rgba(126, 184, 240, 0.32);
141
+ --bajo-strong: #A3CDF5;
142
+
143
+ --urgent: #F08478;
144
+ --monitor: #E5B261;
145
+ --monitor-bg: rgba(229, 178, 97, 0.14);
146
+ }
147
+
148
+ *,
149
+ *::before,
150
+ *::after {
151
+ box-sizing: border-box;
152
+ margin: 0;
153
+ padding: 0;
154
+ }
155
+
156
+ html, body {
157
+ height: 100%;
158
+ overflow: hidden;
159
+ background: var(--surface-page);
160
+ color: var(--text-1);
161
+ font-family: var(--font-sans);
162
+ font-size: var(--fs-base);
163
+ line-height: var(--lh-normal);
164
+ transition: background-color var(--dur-base), color var(--dur-base);
165
+ }
166
+
167
+ body {
168
+ display: flex;
169
+ flex-direction: column;
170
+ }
171
+
172
+ /* HEADER */
173
+ header {
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 20px;
177
+ padding: var(--space-3) var(--space-6) var(--space-3) var(--space-4);
178
+ background: var(--surface-header);
179
+ border-bottom: none;
180
+ flex-wrap: wrap;
181
+ flex-shrink: 0;
182
+ }
183
+
184
+ header #logo {
185
+ color: var(--header-text-muted);
186
+ }
187
+
188
+ header .barra-paciente label {
189
+ color: var(--header-text-muted);
190
+ }
191
+
192
+ header .barra-paciente select,
193
+ header .barra-paciente input {
194
+ background: var(--header-input-bg);
195
+ border-color: var(--header-input-border);
196
+ color: var(--header-text);
197
+ }
198
+
199
+ header .barra-paciente select:focus,
200
+ header .barra-paciente input:focus {
201
+ border-color: var(--header-input-border-focus);
202
+ box-shadow: 0 0 0 3px var(--header-focus-ring);
203
+ }
204
+
205
+ #pt-especie:has(option[value=""]:checked),
206
+ #pt-sexo:has(option[value=""]:checked),
207
+ #pt-edad-unidad {
208
+ color: var(--header-text-muted);
209
+ }
210
+
211
+ #mob-pt-especie:has(option[value=""]:checked),
212
+ #mob-pt-sexo:has(option[value=""]:checked),
213
+ #mob-pt-edad-unidad {
214
+ color: var(--color-placeholder);
215
+ }
216
+
217
+ select:has(option[value=""]:checked) {
218
+ color: var(--color-placeholder);
219
+ }
220
+
221
+ header .barra-paciente select option {
222
+ background: var(--surface-1);
223
+ color: var(--text-1);
224
+ }
225
+
226
+ header .boton-tema,
227
+ header .boton-usuario {
228
+ color: var(--header-text-action);
229
+ }
230
+
231
+ header .boton-tema:hover,
232
+ header .boton-usuario:hover {
233
+ color: var(--header-text-hover);
234
+ }
235
+
236
+ #logo {
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 0.4rem;
240
+ font-size: var(--fs-xl);
241
+ font-weight: 300;
242
+ letter-spacing: var(--tracking-widest);
243
+ color: var(--text-1);
244
+ margin-right: auto;
245
+ text-transform: uppercase;
246
+ }
247
+
248
+ #logo svg {
249
+ width: 1.4em;
250
+ height: 1.4em;
251
+ fill: currentColor;
252
+ flex-shrink: 0;
253
+ }
254
+
255
+ .barra-paciente {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 12px;
259
+ flex-wrap: wrap;
260
+ }
261
+
262
+ .barra-paciente-titulo {
263
+ display: none;
264
+ }
265
+
266
+ .barra-paciente label {
267
+ color: var(--text-3);
268
+ font-size: var(--fs-sm);
269
+ font-weight: 500;
270
+ }
271
+
272
+ button {
273
+ font-family: inherit;
274
+ font-size: inherit;
275
+ background: none;
276
+ border: none;
277
+ cursor: pointer;
278
+ }
279
+
280
+ input::placeholder,
281
+ textarea::placeholder {
282
+ color: var(--color-placeholder);
283
+ }
284
+
285
+ .barra-paciente select,
286
+ .barra-paciente input {
287
+ background: var(--surface-input);
288
+ border: 1px solid var(--border-1);
289
+ border-radius: var(--radius-sm);
290
+ color: var(--text-1);
291
+ padding: 5px var(--space-2);
292
+ font-size: var(--fs-ui);
293
+ font-family: var(--font-sans);
294
+ outline: none;
295
+ transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
296
+ }
297
+
298
+ .barra-paciente select:focus,
299
+ .barra-paciente input:focus {
300
+ border-color: var(--border-focus);
301
+ box-shadow: 0 0 0 3px var(--focus-ring);
302
+ }
303
+
304
+ .barra-paciente select option {
305
+ color: var(--text-2);
306
+ background: var(--surface-input);
307
+ }
308
+
309
+ .barra-paciente select {
310
+ min-width: 110px;
311
+ cursor: pointer;
312
+ }
313
+
314
+ .barra-paciente input {
315
+ width: 7.5rem;
316
+ }
317
+
318
+ /* ACCIONES CABECERA */
319
+ .acciones-cabecera {
320
+ display: flex;
321
+ gap: var(--space-2);
322
+ align-items: center;
323
+ flex-shrink: 0;
324
+ margin-left: 4rem;
325
+ }
326
+
327
+ /* GRID PRINCIPAL */
328
+ main {
329
+ display: grid;
330
+ grid-template-columns: 1fr 1fr 40rem 30rem;
331
+ grid-template-rows: 1fr auto auto;
332
+ grid-template-areas:
333
+ "hema bioquim col3 resultados"
334
+ "endo uri col3 resultados"
335
+ "flujo flujo col3 resultados";
336
+ gap: 1px;
337
+ background: var(--border-1);
338
+ flex: 1;
339
+ min-height: 0;
340
+ overflow: hidden;
341
+ }
342
+
343
+ #panel-hema { grid-area: hema; }
344
+ #panel-bioquim { grid-area: bioquim; }
345
+ #panel-endo { grid-area: endo; }
346
+ #panel-uri { grid-area: uri; }
347
+ #panel-resultados { grid-area: resultados; }
348
+ .panel-flujo { grid-area: flujo; }
349
+
350
+ .col3-wrapper {
351
+ grid-area: col3;
352
+ display: flex;
353
+ flex-direction: column;
354
+ background: var(--border-1);
355
+ gap: 1px;
356
+ overflow: hidden;
357
+ }
358
+
359
+ #panel-imagenes {
360
+ flex: 1;
361
+ min-height: 0;
362
+ }
363
+
364
+ #panel-clinico {
365
+ flex-shrink: 0;
366
+ }
367
+
368
+
369
+ /* HEMA / BIOQUIM — contenido más grande */
370
+ #panel-hema,
371
+ #panel-bioquim {
372
+ --fs-xs: 12px;
373
+ --fs-sm: 0.9rem;
374
+ --fs-ui: 15px;
375
+ }
376
+
377
+ #panel-hema .panel-cuerpo,
378
+ #panel-bioquim .panel-cuerpo {
379
+ padding: 12px 1rem;
380
+ }
381
+
382
+ #panel-hema .fila-campo,
383
+ #panel-bioquim .fila-campo {
384
+ margin-bottom: 9px;
385
+ gap: 0.6rem;
386
+ }
387
+
388
+ #panel-hema .fila-campo label,
389
+ #panel-bioquim .fila-campo label {
390
+ font-size: var(--fs-ui);
391
+ }
392
+
393
+ #panel-hema .fila-campo input,
394
+ #panel-bioquim .fila-campo input {
395
+ width: calc(6rem + 2rem);
396
+ padding: 6px 0.6rem;
397
+ font-size: var(--fs-ui);
398
+ }
399
+
400
+ #panel-hema .fila-campo .unidad,
401
+ #panel-bioquim .fila-campo .unidad {
402
+ min-width: 4rem;
403
+ font-size: var(--fs-ui);
404
+ }
405
+
406
+ .panel {
407
+ background: var(--surface-1);
408
+ display: flex;
409
+ flex-direction: column;
410
+ overflow: hidden;
411
+ }
412
+
413
+ .panel-cabecera {
414
+ height: 2.5rem;
415
+ display: flex;
416
+ align-items: center;
417
+ padding: 0 var(--space-4);
418
+ font-size: clamp(13px, 12.6px + 0.09vw, 14px);
419
+ font-weight: 600;
420
+ letter-spacing: var(--tracking-wide);
421
+ text-transform: uppercase;
422
+ color: var(--text-3);
423
+ border-bottom: 1px solid var(--border-1);
424
+ background: var(--surface-2);
425
+ flex-shrink: 0;
426
+ }
427
+
428
+
429
+
430
+ .panel-cuerpo {
431
+ flex: 1;
432
+ overflow-y: auto;
433
+ overflow-x: hidden;
434
+ padding: var(--space-3) var(--space-4);
435
+ scrollbar-width: thin;
436
+ scrollbar-color: var(--border-2) transparent;
437
+ }
438
+
439
+ /* ENCABEZADOS DE COLUMNA */
440
+ .cabecera-columnas {
441
+ display: flex;
442
+ align-items: center;
443
+ gap: var(--space-2);
444
+ padding: 0 0 var(--space-1) 0;
445
+ margin-bottom: var(--space-3);
446
+ border-bottom: 1px solid var(--border-1);
447
+ position: sticky;
448
+ top: 0;
449
+ background: var(--surface-1);
450
+ z-index: 1;
451
+ }
452
+
453
+ .cabecera-columnas span {
454
+ font-size: var(--fs-xs);
455
+ font-weight: 600;
456
+ letter-spacing: var(--tracking-wide);
457
+ text-transform: uppercase;
458
+ color: var(--color-placeholder);
459
+ }
460
+
461
+ .cabecera-columnas span:first-child {
462
+ flex: 1;
463
+ }
464
+ .cabecera-columnas span:nth-child(2) {
465
+ width: calc(5rem + 2rem);
466
+ text-align: center;
467
+ }
468
+ .cabecera-columnas span:nth-child(3) {
469
+ min-width: 50px;
470
+ text-align: right;
471
+ }
472
+
473
+ /* CAMPOS DEL FORMULARIO */
474
+ .grupo-campo {
475
+ margin-bottom: 18px;
476
+ }
477
+
478
+ .titulo-grupo {
479
+ font-size: var(--fs-xs);
480
+ font-weight: 600;
481
+ letter-spacing: var(--tracking-wider);
482
+ text-transform: uppercase;
483
+ color: var(--text-accent);
484
+ margin-bottom: 14px;
485
+ padding-bottom: var(--space-1);
486
+ border-bottom: 1px solid var(--border-1);
487
+ }
488
+
489
+ .fila-campo {
490
+ display: flex;
491
+ align-items: center;
492
+ justify-content: space-between;
493
+ margin-bottom: 6px;
494
+ gap: var(--space-2);
495
+ }
496
+
497
+ .fila-campo label {
498
+ font-size: var(--fs-sm);
499
+ color: var(--text-3);
500
+ flex: 1;
501
+ }
502
+
503
+ .fila-campo .unidad {
504
+ font-size: var(--fs-xs);
505
+ color: var(--color-placeholder);
506
+ white-space: nowrap;
507
+ min-width: 50px;
508
+ text-align: right;
509
+ }
510
+
511
+ .estado-campo {
512
+ font-size: var(--fs-xs);
513
+ font-weight: 600;
514
+ white-space: nowrap;
515
+ flex-shrink: 0;
516
+ letter-spacing: 0.16px;
517
+ }
518
+
519
+ .estado-campo--alto {
520
+ color: var(--alto);
521
+ }
522
+ .estado-campo--bajo {
523
+ color: var(--bajo);
524
+ }
525
+
526
+ .fila-campo input {
527
+ width: calc(5rem + 2rem);
528
+ background: var(--surface-input);
529
+ border: 1px solid var(--border-1);
530
+ border-radius: var(--radius-sm);
531
+ color: var(--text-1);
532
+ padding: var(--space-1) var(--space-2);
533
+ font-size: var(--fs-ui);
534
+ font-family: var(--font-mono);
535
+ font-variant-numeric: tabular-nums;
536
+ text-align: right;
537
+ outline: none;
538
+ appearance: none;
539
+ transition: border-color var(--dur-fast), background-color var(--dur-fast), color var(--dur-fast);
540
+ }
541
+
542
+ .fila-campo input[type="number"]::-webkit-outer-spin-button,
543
+ .fila-campo input[type="number"]::-webkit-inner-spin-button {
544
+ display: none;
545
+ }
546
+
547
+ .fila-campo input:focus {
548
+ border-color: var(--border-focus);
549
+ box-shadow: 0 0 0 3px var(--focus-ring);
550
+ }
551
+
552
+ .fila-campo input.alto,
553
+ .fila-campo input.alto:focus {
554
+ border-color: var(--alto);
555
+ background: var(--alto-bg);
556
+ color: var(--alto-strong);
557
+ }
558
+
559
+ .fila-campo input.bajo,
560
+ .fila-campo input.bajo:focus {
561
+ border-color: var(--bajo);
562
+ background: var(--bajo-bg);
563
+ color: var(--bajo-strong);
564
+ }
565
+
566
+ .fila-campo input.max-chars,
567
+ .fila-campo input.max-chars:focus {
568
+ border-color: var(--monitor);
569
+ background: var(--monitor-bg);
570
+ }
571
+
572
+ /* PANEL COLUMNA MEDIA (urianálisis + endocrino + imágenes) */
573
+ #panel-uri,
574
+ #panel-endo {
575
+ --fs-ui: 15px;
576
+ --fs-xs: 12px;
577
+ --fs-sm: 0.9rem;
578
+ }
579
+
580
+ #panel-imagenes {
581
+ --fs-ui: 15px;
582
+ --fs-xs: 12px;
583
+ --fs-sm: 0.9rem;
584
+ overflow-y: auto;
585
+ overflow-x: hidden;
586
+ scrollbar-width: thin;
587
+ scrollbar-color: var(--border-2) transparent;
588
+ flex: 1;
589
+ }
590
+
591
+ #panel-imagenes .panel-cabecera {
592
+ position: sticky;
593
+ top: 0;
594
+ z-index: 2;
595
+ border-top: 1px solid var(--border-1);
596
+ }
597
+
598
+ #panel-imagenes .panel-cabecera:first-child {
599
+ border-top: none;
600
+ }
601
+
602
+ .col3-wrapper > .panel.subpanel:first-child > .panel-cabecera {
603
+ border-top: none;
604
+ }
605
+
606
+ .subpanel-anim {
607
+ overflow: hidden;
608
+ transition: height 0.28s cubic-bezier(0.4, 0, 0.2, 1);
609
+ }
610
+
611
+ .subpanel-cuerpo {
612
+ flex-shrink: 0;
613
+ padding: 12px 1rem;
614
+ }
615
+
616
+ /* TOOLTIP GLOBAL */
617
+ #tooltip-global {
618
+ position: fixed;
619
+ background: var(--surface-0, #1a1a1a);
620
+ color: #fff;
621
+ border: 1px solid var(--border-1);
622
+ border-radius: var(--radius-sm);
623
+ padding: 3px 8px;
624
+ font-size: 11px;
625
+ font-weight: 500;
626
+ white-space: nowrap;
627
+ pointer-events: none;
628
+ opacity: 0;
629
+ transition: opacity 0.15s;
630
+ z-index: 10000;
631
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
632
+ }
633
+
634
+ #tooltip-global.visible {
635
+ opacity: 1;
636
+ }
637
+
638
+ /* BOTÓN LIMPIAR PANEL */
639
+ .btn-limpiar-panel {
640
+ margin-left: auto;
641
+ display: flex;
642
+ align-items: center;
643
+ justify-content: center;
644
+ width: 1.875rem;
645
+ height: 1.875rem;
646
+ flex-shrink: 0;
647
+ border: none;
648
+ background: transparent;
649
+ color: var(--text-4);
650
+ cursor: pointer;
651
+ border-radius: var(--radius-sm);
652
+ transition: background 0.15s, color 0.15s;
653
+ }
654
+
655
+ .btn-limpiar-panel svg {
656
+ width: 17.5px;
657
+ height: 17.5px;
658
+ fill: currentColor;
659
+ }
660
+
661
+ .btn-limpiar-panel:hover {
662
+ background: var(--alto-bg);
663
+ color: var(--alto);
664
+ }
665
+
666
+ /* BOTÓN IMPORTAR PDF */
667
+ .btn-importar-pdf {
668
+ margin-left: 2px;
669
+ display: flex;
670
+ align-items: center;
671
+ justify-content: center;
672
+ width: 1.875rem;
673
+ height: 1.875rem;
674
+ flex-shrink: 0;
675
+ border: none;
676
+ background: transparent;
677
+ color: var(--text-4);
678
+ cursor: pointer;
679
+ border-radius: var(--radius-sm);
680
+ transition: background 0.15s, color 0.15s;
681
+ }
682
+
683
+ .btn-importar-pdf svg {
684
+ width: 17.5px;
685
+ height: 17.5px;
686
+ fill: currentColor;
687
+ }
688
+
689
+ .btn-importar-pdf:hover {
690
+ background: var(--border-1);
691
+ color: var(--accent);
692
+ }
693
+
694
+ .btn-importar-pdf + .btn-colapsar-subpanel {
695
+ margin-left: 2px;
696
+ }
697
+
698
+ /* TOAST NOTIFICACIÓN PDF */
699
+ .pdf-toast {
700
+ position: fixed;
701
+ bottom: 2rem;
702
+ left: 50%;
703
+ transform: translateX(-50%) translateY(6px);
704
+ background: var(--surface-1);
705
+ color: var(--text-1);
706
+ border: 1px solid var(--border-1);
707
+ border-radius: var(--radius-md);
708
+ padding: var(--space-2) var(--space-4);
709
+ font-size: var(--fs-sm);
710
+ font-weight: 500;
711
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.14);
712
+ opacity: 0;
713
+ transition: opacity 0.2s, transform 0.2s;
714
+ pointer-events: none;
715
+ z-index: 9999;
716
+ white-space: nowrap;
717
+ }
718
+
719
+ .pdf-toast--show {
720
+ opacity: 1;
721
+ transform: translateX(-50%) translateY(0);
722
+ }
723
+
724
+ .pdf-toast--error {
725
+ border-color: var(--alto);
726
+ color: var(--alto);
727
+ }
728
+
729
+ /* BOTÓN COLAPSAR SUBPANEL */
730
+ .btn-colapsar-subpanel {
731
+ margin-left: auto;
732
+ display: flex;
733
+ align-items: center;
734
+ justify-content: center;
735
+ width: 1.875rem;
736
+ height: 1.875rem;
737
+ flex-shrink: 0;
738
+ border: none;
739
+ background: transparent;
740
+ color: var(--text-3);
741
+ cursor: pointer;
742
+ border-radius: var(--radius-sm, 4px);
743
+ transition: background 0.15s, color 0.15s;
744
+ }
745
+
746
+ .btn-colapsar-subpanel:hover {
747
+ background: var(--border-1);
748
+ color: var(--text-1);
749
+ }
750
+
751
+ .btn-colapsar-subpanel svg {
752
+ width: 16.25px;
753
+ height: 16.25px;
754
+ fill: currentColor;
755
+ }
756
+
757
+ .btn-colapsar-subpanel .icono-colapsar {
758
+ display: none;
759
+ }
760
+
761
+ .subpanel.collapsed .btn-colapsar-subpanel .icono-expandir {
762
+ display: none;
763
+ }
764
+
765
+ .subpanel.collapsed .btn-colapsar-subpanel .icono-colapsar {
766
+ display: block;
767
+ }
768
+
769
+ .fila-campo select {
770
+ width: calc(5rem + 2rem);
771
+ background: var(--surface-input);
772
+ border: 1px solid var(--border-1);
773
+ border-radius: var(--radius-sm);
774
+ color: var(--text-1);
775
+ padding: var(--space-1) var(--space-2);
776
+ font-size: var(--fs-ui);
777
+ font-family: var(--font-sans);
778
+ outline: none;
779
+ cursor: pointer;
780
+ transition: border-color var(--dur-fast), background-color var(--dur-fast);
781
+ }
782
+
783
+ .fila-campo select:focus {
784
+ border-color: var(--border-focus);
785
+ box-shadow: 0 0 0 3px var(--focus-ring);
786
+ }
787
+
788
+
789
+ /* CHAT AREA (signos clínicos en resultados) */
790
+ .chat-area {
791
+ padding: 0.5rem var(--space-4);
792
+ border-top: 1px solid var(--border-1);
793
+ background: var(--surface-2);
794
+ flex-shrink: 0;
795
+ }
796
+
797
+ .chat-area textarea {
798
+ width: 100%;
799
+ box-sizing: border-box;
800
+ background: var(--surface-input);
801
+ border: 1px solid var(--border-1);
802
+ border-radius: var(--radius-sm);
803
+ color: var(--text-1);
804
+ padding: var(--space-2) var(--space-3);
805
+ font-size: var(--fs-ui);
806
+ font-family: var(--font-sans);
807
+ resize: none;
808
+ height: 4rem;
809
+ outline: none;
810
+ line-height: var(--lh-normal);
811
+ transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
812
+ }
813
+
814
+ .chat-area textarea:focus {
815
+ border-color: var(--border-focus);
816
+ box-shadow: 0 0 0 3px var(--focus-ring);
817
+ }
818
+
819
+ /* PANEL DE RESULTADOS */
820
+
821
+ .seccion-resultado {
822
+ margin-bottom: 20px;
823
+ }
824
+
825
+ .titulo-seccion-resultado {
826
+ font-size: var(--fs-xs);
827
+ font-weight: 600;
828
+ letter-spacing: var(--tracking-wider);
829
+ text-transform: uppercase;
830
+ color: var(--text-accent);
831
+ margin-bottom: var(--space-2);
832
+ }
833
+
834
+ .titulo-patrones {
835
+ display: flex;
836
+ align-items: center;
837
+ gap: var(--space-2);
838
+ cursor: pointer;
839
+ }
840
+
841
+ .btn-colapsar-patrones {
842
+ margin-left: auto;
843
+ display: flex;
844
+ align-items: center;
845
+ background: none;
846
+ border: none;
847
+ cursor: pointer;
848
+ color: var(--text-4);
849
+ padding: 2px;
850
+ border-radius: var(--radius-sm);
851
+ transition: color var(--dur-fast), transform var(--dur-base);
852
+ }
853
+
854
+ .btn-colapsar-patrones:hover {
855
+ color: var(--text-accent);
856
+ }
857
+
858
+ .btn-colapsar-patrones svg {
859
+ width: 12px;
860
+ height: 12px;
861
+ fill: currentColor;
862
+ }
863
+
864
+ .btn-colapsar-patrones .icono-colapsar {
865
+ display: none;
866
+ }
867
+
868
+ .btn-colapsar-patrones[aria-expanded="false"] .icono-expandir {
869
+ display: none;
870
+ }
871
+
872
+ .btn-colapsar-patrones[aria-expanded="false"] .icono-colapsar {
873
+ display: block;
874
+ }
875
+
876
+ .patrones-anim {
877
+ overflow: hidden;
878
+ transition: height 0.28s cubic-bezier(0.4, 0, 0.2, 1);
879
+ }
880
+
881
+ /* PATRONES */
882
+ .elemento-patron {
883
+ background: var(--surface-2);
884
+ border: 1px solid var(--border-1);
885
+ border-left: 3px solid var(--accent);
886
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
887
+ padding: var(--space-2) var(--space-3);
888
+ margin-bottom: var(--space-2);
889
+ }
890
+
891
+ .elemento-patron .titulo-patron {
892
+ font-size: var(--fs-xs);
893
+ font-weight: 600;
894
+ letter-spacing: var(--tracking-wider);
895
+ text-transform: uppercase;
896
+ color: var(--text-accent);
897
+ margin-bottom: 2px;
898
+ }
899
+
900
+ .elemento-patron .cuerpo-patron {
901
+ font-size: var(--fs-sm);
902
+ color: var(--text-2);
903
+ line-height: var(--lh-normal);
904
+ }
905
+
906
+ .elemento-patron.gravedad-grave {
907
+ border-left-color: var(--urgent);
908
+ }
909
+
910
+ .elemento-patron.gravedad-grave .titulo-patron {
911
+ color: var(--urgent);
912
+ }
913
+
914
+ .elemento-patron.gravedad-moderado {
915
+ border-left-color: var(--monitor);
916
+ }
917
+
918
+ .elemento-patron.gravedad-moderado .titulo-patron {
919
+ color: var(--monitor);
920
+ }
921
+
922
+ .sin-hallazgos {
923
+ font-size: var(--fs-sm);
924
+ color: var(--text-4);
925
+ font-style: italic;
926
+ }
927
+
928
+ /* IA OUTPUT */
929
+ #salida-ia {
930
+ font-size: var(--fs-ui);
931
+ color: var(--text-3);
932
+ line-height: 1.7;
933
+ white-space: pre-wrap;
934
+ background: var(--surface-2);
935
+ border: 1px solid var(--border-1);
936
+ border-radius: var(--radius-md);
937
+ padding: var(--space-3) var(--space-4);
938
+ }
939
+
940
+ /* FOOTER */
941
+ footer {
942
+ display: flex;
943
+ align-items: center;
944
+ gap: var(--space-4);
945
+ padding: var(--space-2) var(--space-6);
946
+ background: var(--surface-1);
947
+ border-top: 1px solid var(--border-1);
948
+ flex-wrap: wrap;
949
+ flex-shrink: 0;
950
+ }
951
+
952
+ #aviso {
953
+ flex: 1;
954
+ font-size: var(--fs-xs);
955
+ color: var(--color-placeholder);
956
+ line-height: var(--lh-snug);
957
+ }
958
+
959
+ #aviso strong {
960
+ color: var(--color-placeholder);
961
+ }
962
+
963
+ .btn-adjuntar-mob {
964
+ display: none;
965
+ width: 100%;
966
+ justify-content: center;
967
+ gap: var(--space-2);
968
+ margin-top: var(--space-4);
969
+ background: var(--accent);
970
+ color: var(--text-on-accent);
971
+ border-color: var(--accent);
972
+ min-width: 100px;
973
+ }
974
+
975
+ .btn-adjuntar-mob:hover {
976
+ background: var(--accent-hover);
977
+ border-color: var(--accent-hover);
978
+ }
979
+
980
+ .btn-adjuntar-mob svg {
981
+ width: 18px;
982
+ height: 18px;
983
+ fill: currentColor;
984
+ }
985
+
986
+ #aviso-mob {
987
+ display: none;
988
+ font-size: var(--fs-xs);
989
+ color: var(--color-placeholder);
990
+ line-height: var(--lh-snug);
991
+ padding: var(--space-3) 0 var(--space-1);
992
+ margin-top: 3rem;
993
+ }
994
+
995
+ #aviso-mob strong {
996
+ color: var(--color-placeholder);
997
+ }
998
+
999
+ #creditos-mob {
1000
+ display: none;
1001
+ align-items: center;
1002
+ justify-content: center;
1003
+ gap: var(--space-1);
1004
+ font-size: var(--fs-xs);
1005
+ color: var(--color-placeholder);
1006
+ white-space: nowrap;
1007
+ padding: var(--space-3) 0;
1008
+ margin-top: var(--space-2);
1009
+ border-top: 1px solid var(--border-1);
1010
+ }
1011
+
1012
+ #creditos-mob svg {
1013
+ width: 14px;
1014
+ height: 14px;
1015
+ fill: currentColor;
1016
+ flex-shrink: 0;
1017
+ }
1018
+
1019
+ #creditos-mob a {
1020
+ color: inherit;
1021
+ text-decoration: none;
1022
+ }
1023
+
1024
+ #creditos-mob a:hover {
1025
+ color: var(--text-accent);
1026
+ }
1027
+
1028
+ #creditos {
1029
+ display: flex;
1030
+ align-items: center;
1031
+ gap: var(--space-1);
1032
+ font-size: var(--fs-xs);
1033
+ color: var(--color-placeholder);
1034
+ white-space: nowrap;
1035
+ }
1036
+
1037
+ #creditos svg {
1038
+ width: 14px;
1039
+ height: 14px;
1040
+ fill: currentColor;
1041
+ flex-shrink: 0;
1042
+ }
1043
+
1044
+ #creditos a {
1045
+ color: inherit;
1046
+ text-decoration: none;
1047
+ }
1048
+
1049
+ #creditos a:hover {
1050
+ color: var(--text-accent);
1051
+ }
1052
+
1053
+ .acciones-pie {
1054
+ display: flex;
1055
+ gap: var(--space-2);
1056
+ flex-shrink: 0;
1057
+ }
1058
+
1059
+ /* BOTONES */
1060
+ .boton {
1061
+ padding: 7px 16px;
1062
+ border-radius: var(--radius-sm);
1063
+ border: 1px solid transparent;
1064
+ font-size: var(--fs-ui);
1065
+ font-weight: 500;
1066
+ font-family: var(--font-sans);
1067
+ cursor: pointer;
1068
+ transition:
1069
+ background-color var(--dur-fast),
1070
+ color var(--dur-fast),
1071
+ border-color var(--dur-fast),
1072
+ opacity var(--dur-fast);
1073
+ white-space: nowrap;
1074
+ display: inline-flex;
1075
+ align-items: center;
1076
+ gap: var(--space-1);
1077
+ }
1078
+
1079
+ .boton:disabled {
1080
+ opacity: 0.4;
1081
+ cursor: not-allowed;
1082
+ }
1083
+
1084
+
1085
+ .boton-analizar {
1086
+ background: var(--accent);
1087
+ color: var(--text-on-accent);
1088
+ border-color: var(--accent);
1089
+ min-width: 100px;
1090
+ }
1091
+
1092
+ .boton-analizar:hover:not(:disabled) {
1093
+ background: var(--accent-hover);
1094
+ border-color: var(--accent-hover);
1095
+ }
1096
+
1097
+
1098
+ #salida-ia.cargando {
1099
+ color: var(--text-4);
1100
+ font-style: italic;
1101
+ }
1102
+
1103
+ /* IA BACKEND CONFIG */
1104
+
1105
+ .ia-backend-config {
1106
+ display: flex;
1107
+ flex-wrap: wrap;
1108
+ align-items: center;
1109
+ gap: var(--space-2) var(--space-3);
1110
+ margin-top: var(--space-3);
1111
+ padding-top: var(--space-3);
1112
+ border-top: 1px solid var(--border-1);
1113
+ }
1114
+
1115
+ .ia-backend-label {
1116
+ font-size: var(--fs-xs);
1117
+ color: var(--text-4);
1118
+ font-weight: 500;
1119
+ white-space: nowrap;
1120
+ }
1121
+
1122
+ .ia-backend-opt {
1123
+ display: inline-flex;
1124
+ align-items: center;
1125
+ gap: var(--space-1);
1126
+ font-size: var(--fs-xs);
1127
+ color: var(--text-3);
1128
+ cursor: pointer;
1129
+ white-space: nowrap;
1130
+ }
1131
+
1132
+ .ia-backend-opt input[type="radio"] {
1133
+ accent-color: var(--accent);
1134
+ cursor: pointer;
1135
+ }
1136
+
1137
+ .ia-ollama-fields {
1138
+ display: flex;
1139
+ flex-wrap: wrap;
1140
+ gap: var(--space-2);
1141
+ width: 100%;
1142
+ }
1143
+
1144
+ .ia-text-input {
1145
+ flex: 1;
1146
+ min-width: 140px;
1147
+ padding: 4px 8px;
1148
+ font-size: var(--fs-xs);
1149
+ font-family: var(--font-mono, monospace);
1150
+ background: var(--surface-1);
1151
+ border: 1px solid var(--border-1);
1152
+ border-radius: var(--radius-sm);
1153
+ color: var(--text-2);
1154
+ }
1155
+
1156
+ .ia-text-input:focus {
1157
+ outline: 2px solid var(--accent);
1158
+ outline-offset: 1px;
1159
+ }
1160
+
1161
+ #ia-ollama-url,
1162
+ #ia-ollama-model {
1163
+ color: var(--color-placeholder);
1164
+ }
1165
+
1166
+ .ia-model-input {
1167
+ max-width: 130px;
1168
+ flex: 0 0 auto;
1169
+ }
1170
+
1171
+ .ia-hf-fields {
1172
+ width: 100%;
1173
+ }
1174
+
1175
+ .ia-select {
1176
+ width: 100%;
1177
+ padding: 4px 8px;
1178
+ font-size: var(--fs-xs);
1179
+ font-family: var(--font-sans);
1180
+ background: var(--surface-1);
1181
+ border: 1px solid var(--border-1);
1182
+ border-radius: var(--radius-sm);
1183
+ color: var(--text-2);
1184
+ cursor: pointer;
1185
+ }
1186
+
1187
+ .ia-select:focus {
1188
+ outline: 2px solid var(--accent);
1189
+ outline-offset: 1px;
1190
+ }
1191
+
1192
+ /* ZONAS DE IMAGEN */
1193
+
1194
+ #subpanel-citologia {
1195
+ flex: 1;
1196
+ display: flex;
1197
+ flex-direction: column;
1198
+ min-height: 0;
1199
+ }
1200
+
1201
+ #subpanel-citologia .subpanel-anim {
1202
+ flex: 1;
1203
+ display: flex;
1204
+ flex-direction: column;
1205
+ min-height: 0;
1206
+ }
1207
+
1208
+ #cuerpo-citologia {
1209
+ flex: 1;
1210
+ display: flex;
1211
+ flex-direction: column;
1212
+ min-height: 0;
1213
+ }
1214
+
1215
+ #cuerpo-citologia .zonas-imagen {
1216
+ flex-shrink: 0;
1217
+ }
1218
+
1219
+ .zonas-imagen {
1220
+ display: grid;
1221
+ grid-template-columns: 1fr 1fr;
1222
+ gap: var(--space-3);
1223
+ }
1224
+
1225
+ .zona-imagen {
1226
+ position: relative;
1227
+ height: 110px;
1228
+ border: 1.5px dashed var(--border-2);
1229
+ border-radius: var(--radius-md);
1230
+ cursor: pointer;
1231
+ overflow: hidden;
1232
+ display: flex;
1233
+ align-items: center;
1234
+ justify-content: center;
1235
+ transition: border-color var(--dur-fast);
1236
+ }
1237
+ .zona-imagen:hover {
1238
+ border-color: var(--accent);
1239
+ }
1240
+ .zona-imagen.con-imagen {
1241
+ border-style: solid;
1242
+ border-color: var(--border-1);
1243
+ }
1244
+
1245
+ .zona-vacia svg {
1246
+ width: 22px;
1247
+ height: 22px;
1248
+ fill: currentColor;
1249
+ }
1250
+
1251
+ .zona-vacia {
1252
+ display: flex;
1253
+ flex-direction: column;
1254
+ align-items: center;
1255
+ gap: var(--space-2);
1256
+ color: var(--color-placeholder);
1257
+ pointer-events: none;
1258
+ font-size: var(--fs-xs);
1259
+ }
1260
+
1261
+ /* ZONA MICROSCOPIO */
1262
+
1263
+ .zona-microscopio {
1264
+ position: relative;
1265
+ flex: 1;
1266
+ width: 100%;
1267
+ min-height: 150px;
1268
+ margin-top: var(--space-3);
1269
+ border: 1.5px dashed var(--border-2);
1270
+ border-radius: var(--radius-md);
1271
+ overflow: hidden;
1272
+ display: flex;
1273
+ align-items: center;
1274
+ justify-content: center;
1275
+ cursor: pointer;
1276
+ transition: border-color var(--dur-fast);
1277
+ background: var(--surface-1);
1278
+ }
1279
+
1280
+ .zona-microscopio:hover {
1281
+ border-color: var(--accent);
1282
+ }
1283
+
1284
+ .micro-vacia {
1285
+ display: flex;
1286
+ flex-direction: column;
1287
+ align-items: center;
1288
+ gap: var(--space-2);
1289
+ color: var(--color-placeholder);
1290
+ font-size: var(--fs-xs);
1291
+ pointer-events: none;
1292
+ }
1293
+
1294
+ .micro-video {
1295
+ position: absolute;
1296
+ inset: 0;
1297
+ width: 100%;
1298
+ height: 100%;
1299
+ object-fit: cover;
1300
+ }
1301
+
1302
+ /* control bar */
1303
+ .micro-controles {
1304
+ position: absolute;
1305
+ bottom: 0;
1306
+ left: 0;
1307
+ right: 0;
1308
+ display: flex;
1309
+ align-items: center;
1310
+ justify-content: space-between;
1311
+ padding: var(--space-2) var(--space-3);
1312
+ background: var(--surface-2);
1313
+ border-top: 1px solid var(--border-1);
1314
+ gap: var(--space-2);
1315
+ }
1316
+
1317
+ .micro-vacia svg {
1318
+ width: 22px;
1319
+ height: 22px;
1320
+ fill: currentColor;
1321
+ }
1322
+
1323
+ .micro-btn svg {
1324
+ width: 16px;
1325
+ height: 16px;
1326
+ fill: currentColor;
1327
+ }
1328
+
1329
+ .micro-btn-capturar svg {
1330
+ width: 20px;
1331
+ height: 20px;
1332
+ }
1333
+
1334
+ .micro-btn {
1335
+ position: relative;
1336
+ display: flex;
1337
+ align-items: center;
1338
+ justify-content: center;
1339
+ width: 2.25rem;
1340
+ height: 2.25rem;
1341
+ border-radius: 50%;
1342
+ color: var(--text-3);
1343
+ background: var(--surface-3);
1344
+ border: 1px solid var(--border-1);
1345
+ flex-shrink: 0;
1346
+ transition: background var(--dur-fast), border-color var(--dur-fast), color var(--dur-fast);
1347
+ }
1348
+
1349
+ .micro-btn:hover {
1350
+ background: var(--border-1);
1351
+ border-color: var(--border-2);
1352
+ color: var(--text-1);
1353
+ }
1354
+
1355
+ .micro-btn-capturar {
1356
+ width: 3rem;
1357
+ height: 3rem;
1358
+ background: var(--accent);
1359
+ border-color: var(--accent);
1360
+ color: var(--text-on-accent);
1361
+ }
1362
+
1363
+ .micro-btn-capturar:hover {
1364
+ background: var(--accent-hover);
1365
+ border-color: var(--accent-hover);
1366
+ }
1367
+
1368
+ .micro-btn-capturar:disabled {
1369
+ opacity: 0.35;
1370
+ cursor: not-allowed;
1371
+ }
1372
+
1373
+ .micro-badge {
1374
+ position: absolute;
1375
+ top: -4px;
1376
+ right: -4px;
1377
+ min-width: 16px;
1378
+ height: 16px;
1379
+ padding: 0 3px;
1380
+ border-radius: 8px;
1381
+ background: var(--accent);
1382
+ color: var(--text-on-accent);
1383
+ font-size: 10px;
1384
+ font-weight: 700;
1385
+ font-family: var(--font-sans);
1386
+ display: flex;
1387
+ align-items: center;
1388
+ justify-content: center;
1389
+ border: 1.5px solid var(--surface-1);
1390
+ }
1391
+
1392
+ /* panel-galeria */
1393
+ .micro-galeria {
1394
+ position: absolute;
1395
+ bottom: 3.5rem;
1396
+ left: 0;
1397
+ right: 0;
1398
+ background: var(--surface-1);
1399
+ border-top: 1px solid var(--border-1);
1400
+ padding: var(--space-2) var(--space-3);
1401
+ display: flex;
1402
+ gap: var(--space-2);
1403
+ align-items: center;
1404
+ }
1405
+
1406
+ .micro-galeria-vacia {
1407
+ font-size: var(--fs-xs);
1408
+ color: var(--text-4);
1409
+ font-style: italic;
1410
+ }
1411
+
1412
+ .micro-thumb {
1413
+ position: relative;
1414
+ width: 52px;
1415
+ height: 52px;
1416
+ border-radius: var(--radius-sm);
1417
+ overflow: hidden;
1418
+ flex-shrink: 0;
1419
+ border: 1px solid var(--border-1);
1420
+ background: var(--surface-2);
1421
+ }
1422
+
1423
+ .micro-thumb img {
1424
+ width: 100%;
1425
+ height: 100%;
1426
+ object-fit: cover;
1427
+ display: block;
1428
+ }
1429
+
1430
+ .micro-thumb-quitar {
1431
+ position: absolute;
1432
+ top: 2px;
1433
+ right: 2px;
1434
+ width: 18px;
1435
+ height: 18px;
1436
+ border-radius: 50%;
1437
+ background: rgba(0, 0, 0, 0.55);
1438
+ color: #fff;
1439
+ display: flex;
1440
+ align-items: center;
1441
+ justify-content: center;
1442
+ transition: background var(--dur-fast);
1443
+ }
1444
+
1445
+ .micro-thumb-quitar:hover {
1446
+ background: rgba(0, 0, 0, 0.82);
1447
+ }
1448
+
1449
+ .zona-img-preview {
1450
+ position: absolute;
1451
+ inset: 0;
1452
+ width: 100%;
1453
+ height: 100%;
1454
+ object-fit: cover;
1455
+ }
1456
+
1457
+ .btn-quitar-zona svg {
1458
+ width: 16px;
1459
+ height: 16px;
1460
+ fill: currentColor;
1461
+ }
1462
+
1463
+ .btn-quitar-zona {
1464
+ position: absolute;
1465
+ top: var(--space-1);
1466
+ right: var(--space-1);
1467
+ width: 20px;
1468
+ height: 20px;
1469
+ border-radius: var(--radius-sm);
1470
+ border: 1px solid var(--border-1);
1471
+ background: var(--surface-2);
1472
+ color: var(--text-3);
1473
+ cursor: pointer;
1474
+ display: flex;
1475
+ align-items: center;
1476
+ justify-content: center;
1477
+ padding: 0;
1478
+ transition: background var(--dur-fast), color var(--dur-fast);
1479
+ }
1480
+ .btn-quitar-zona:hover {
1481
+ background: var(--surface-3);
1482
+ color: var(--text-1);
1483
+ border-color: var(--border-2);
1484
+ }
1485
+
1486
+ /* PANEL PIE DE ACCIONES */
1487
+ .panel-pie-acciones {
1488
+ display: flex;
1489
+ align-items: center;
1490
+ gap: var(--space-2);
1491
+ height: 2.5rem;
1492
+ padding: 0 var(--space-4);
1493
+ padding-bottom: 1rem;
1494
+ background: var(--surface-2);
1495
+ flex-shrink: 0;
1496
+ justify-content: flex-end;
1497
+ }
1498
+
1499
+ .panel-pie-acciones .boton {
1500
+ padding: 7px 19px;
1501
+ }
1502
+
1503
+ .panel-pie-acciones .boton-analizar {
1504
+ min-width: 7.5rem;
1505
+ }
1506
+
1507
+ .boton-papers {
1508
+ background: var(--surface-1);
1509
+ color: var(--text-accent);
1510
+ border-color: var(--accent);
1511
+ }
1512
+
1513
+ .boton-papers:hover:not(:disabled) {
1514
+ background: var(--surface-2);
1515
+ }
1516
+
1517
+ /* THEME TOGGLE */
1518
+ .boton-tema {
1519
+ background: transparent;
1520
+ color: var(--text-2);
1521
+ border: 1px solid var(--border-1);
1522
+ border-radius: var(--radius-sm);
1523
+ padding: 5px 12px;
1524
+ display: inline-flex;
1525
+ align-items: center;
1526
+ min-height: 32px;
1527
+ }
1528
+
1529
+ .boton-tema:hover {
1530
+ background: var(--surface-2);
1531
+ color: var(--text-1);
1532
+ border-color: var(--border-2);
1533
+ }
1534
+
1535
+ .icono-sol {
1536
+ display: none;
1537
+ width: 14px;
1538
+ height: 14px;
1539
+ fill: currentColor;
1540
+ }
1541
+ .icono-luna {
1542
+ display: block;
1543
+ width: 14px;
1544
+ height: 14px;
1545
+ fill: currentColor;
1546
+ }
1547
+
1548
+ [data-theme="dark"] .icono-sol {
1549
+ display: block;
1550
+ }
1551
+ [data-theme="dark"] .icono-luna {
1552
+ display: none;
1553
+ }
1554
+
1555
+
1556
+
1557
+ /* BOTÓN COLAPSAR FLUJO */
1558
+ .btn-colapsar-flujo {
1559
+ margin-left: auto;
1560
+ display: flex;
1561
+ align-items: center;
1562
+ justify-content: center;
1563
+ width: 1.875rem;
1564
+ height: 1.875rem;
1565
+ flex-shrink: 0;
1566
+ border: none;
1567
+ background: transparent;
1568
+ color: var(--text-3);
1569
+ cursor: pointer;
1570
+ border-radius: var(--radius-sm, 4px);
1571
+ transition: background 0.15s, color 0.15s;
1572
+ }
1573
+
1574
+ .btn-colapsar-flujo:hover {
1575
+ background: var(--border-1);
1576
+ color: var(--text-1);
1577
+ }
1578
+
1579
+ .btn-colapsar-flujo svg {
1580
+ width: 16.25px;
1581
+ height: 16.25px;
1582
+ fill: currentColor;
1583
+ }
1584
+
1585
+ .btn-colapsar-flujo .icono-colapsar {
1586
+ display: none;
1587
+ }
1588
+
1589
+ .panel-flujo.collapsed .btn-colapsar-flujo .icono-expandir {
1590
+ display: none;
1591
+ }
1592
+
1593
+ .panel-flujo.collapsed .btn-colapsar-flujo .icono-colapsar {
1594
+ display: block;
1595
+ }
1596
+
1597
+ main {
1598
+ transition: grid-template-rows 0.28s cubic-bezier(0.4, 0, 0.2, 1);
1599
+ }
1600
+
1601
+ #panel-flujo::after {
1602
+ content: '';
1603
+ display: block;
1604
+ height: 0.5rem;
1605
+ flex-shrink: 0;
1606
+ }
1607
+
1608
+ /* PANEL FLUJO DE TRABAJO */
1609
+ .cuerpo-flujo {
1610
+ display: flex;
1611
+ align-items: center;
1612
+ justify-content: center;
1613
+ margin-top: 1.5rem;
1614
+ padding: 0.3rem 20px;
1615
+ overflow: hidden;
1616
+ }
1617
+
1618
+ .pasos-flujo {
1619
+ list-style: none;
1620
+ display: flex;
1621
+ flex-direction: row;
1622
+ width: 100%;
1623
+ }
1624
+
1625
+ .pasos-flujo li {
1626
+ flex: 1;
1627
+ display: flex;
1628
+ flex-direction: row;
1629
+ align-items: flex-start;
1630
+ justify-content: flex-start;
1631
+ text-align: left;
1632
+ gap: 10px;
1633
+ padding: 0 12px;
1634
+ position: relative;
1635
+ }
1636
+
1637
+ .pasos-flujo li + li::before {
1638
+ content: "";
1639
+ position: absolute;
1640
+ left: 0;
1641
+ top: 50%;
1642
+ transform: translateY(-50%);
1643
+ width: 1px;
1644
+ height: 70%;
1645
+ background: var(--border-1);
1646
+ }
1647
+
1648
+ .num-paso {
1649
+ width: 28px;
1650
+ height: 28px;
1651
+ fill: var(--accent);
1652
+ flex-shrink: 0;
1653
+ margin-top: 0.1rem;
1654
+ }
1655
+
1656
+ .pasos-flujo strong {
1657
+ display: block;
1658
+ font-size: var(--fs-xs);
1659
+ color: var(--text-2);
1660
+ font-weight: 600;
1661
+ }
1662
+
1663
+ .pasos-flujo p {
1664
+ font-size: var(--fs-xs);
1665
+ color: var(--text-3);
1666
+ line-height: var(--lh-snug);
1667
+ }
1668
+
1669
+ /* NAV INFERIOR */
1670
+ .nav-inferior {
1671
+ display: none;
1672
+ background: var(--surface-1);
1673
+ border-top: 1px solid var(--border-1);
1674
+ flex-shrink: 0;
1675
+ }
1676
+
1677
+ .tab-nav {
1678
+ flex: 1;
1679
+ display: flex;
1680
+ flex-direction: column;
1681
+ align-items: center;
1682
+ justify-content: center;
1683
+ gap: 0.2rem;
1684
+ color: var(--text-4);
1685
+ font-size: 0.6rem;
1686
+ font-weight: 500;
1687
+ letter-spacing: 0.5px;
1688
+ transition: color var(--dur-fast);
1689
+ padding: 0.4rem 4px 0.3rem;
1690
+ border-top: 2px solid transparent;
1691
+ }
1692
+
1693
+ .tab-nav:hover {
1694
+ color: var(--text-2);
1695
+ }
1696
+
1697
+ .tab-nav.activo {
1698
+ color: var(--accent);
1699
+ border-top-color: var(--accent);
1700
+ }
1701
+
1702
+ .tab-nav svg {
1703
+ width: 20px;
1704
+ height: 20px;
1705
+ flex-shrink: 0;
1706
+ fill: currentColor;
1707
+ }
1708
+
1709
+ /* PANEL PACIENTE */
1710
+
1711
+ .panel-cuerpo--paciente {
1712
+ display: flex;
1713
+ flex-direction: column;
1714
+ gap: var(--space-2);
1715
+ padding: var(--space-3) var(--space-4);
1716
+ }
1717
+
1718
+ .fila-paciente {
1719
+ display: flex;
1720
+ flex-direction: column;
1721
+ gap: var(--space-1);
1722
+ }
1723
+
1724
+ .fila-paciente label {
1725
+ font-size: var(--fs-sm);
1726
+ font-weight: 500;
1727
+ color: var(--text-3);
1728
+ letter-spacing: 0.3px;
1729
+ }
1730
+
1731
+ .fila-paciente select,
1732
+ .fila-paciente input[type="text"],
1733
+ .fila-paciente input[type="number"] {
1734
+ height: 36px;
1735
+ padding: 0 var(--space-3);
1736
+ background: var(--surface-input);
1737
+ border: 1px solid var(--border-1);
1738
+ border-radius: var(--radius-sm);
1739
+ color: var(--text-1);
1740
+ font-size: var(--fs-ui);
1741
+ font-family: var(--font-sans);
1742
+ outline: none;
1743
+ width: 100%;
1744
+ transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
1745
+ }
1746
+
1747
+ .fila-paciente select:focus,
1748
+ .fila-paciente input:focus {
1749
+ border-color: var(--border-focus);
1750
+ box-shadow: 0 0 0 3px var(--focus-ring);
1751
+ }
1752
+
1753
+ #pt-raza::placeholder,
1754
+ #pt-edad::placeholder {
1755
+ color: var(--header-text-muted);
1756
+ }
1757
+
1758
+ #mob-pt-raza::placeholder,
1759
+ #mob-pt-edad::placeholder {
1760
+ color: var(--color-placeholder);
1761
+ }
1762
+
1763
+ .fila-paciente-edad {
1764
+ display: flex;
1765
+ gap: var(--space-2);
1766
+ }
1767
+
1768
+ .fila-paciente-edad input {
1769
+ flex: 1;
1770
+ }
1771
+
1772
+ .fila-paciente-edad select {
1773
+ width: 110px;
1774
+ flex-shrink: 0;
1775
+ }
1776
+
1777
+ /* EXAMENES SUBTABS BAR */
1778
+ #examenes-subtabs-bar {
1779
+ flex-shrink: 0;
1780
+ display: flex;
1781
+ height: 2.5rem;
1782
+ background: var(--surface-2);
1783
+ border-bottom: 1px solid var(--border-1);
1784
+ overflow-x: auto;
1785
+ scrollbar-width: none;
1786
+ }
1787
+
1788
+ .tab-examenes {
1789
+ flex: 1;
1790
+ display: flex;
1791
+ align-items: center;
1792
+ justify-content: center;
1793
+ padding: 0 var(--space-3);
1794
+ font-size: clamp(13px, 12.6px + 0.09vw, 14px);
1795
+ font-weight: 600;
1796
+ letter-spacing: var(--tracking-wide);
1797
+ text-transform: uppercase;
1798
+ color: var(--text-4);
1799
+ border-bottom: 2px solid transparent;
1800
+ white-space: nowrap;
1801
+ transition: color var(--dur-fast), border-color var(--dur-fast);
1802
+ }
1803
+
1804
+ .tab-examenes:hover {
1805
+ color: var(--text-2);
1806
+ }
1807
+
1808
+ .tab-examenes.activo {
1809
+ color: var(--text-3);
1810
+ border-bottom-color: var(--accent);
1811
+ }
1812
+
1813
+ /* SOLO DESKTOP */
1814
+ @media (min-width: 1101px) {
1815
+ .cabecera-columnas--mobile-only {
1816
+ display: none;
1817
+ }
1818
+
1819
+ .cabecera-col-btns {
1820
+ display: none;
1821
+ }
1822
+
1823
+ #panel-paciente,
1824
+ #examenes-subtabs-bar {
1825
+ display: none;
1826
+ }
1827
+
1828
+ .subpanel > .panel-cabecera,
1829
+ #panel-flujo > .panel-cabecera {
1830
+ cursor: default;
1831
+ }
1832
+
1833
+ .subpanel > .panel-cabecera button,
1834
+ #panel-flujo > .panel-cabecera button {
1835
+ cursor: pointer;
1836
+ }
1837
+
1838
+ #panel-hema .subpanel-anim,
1839
+ #panel-bioquim .subpanel-anim {
1840
+ flex: 1;
1841
+ min-height: 0;
1842
+ display: flex;
1843
+ flex-direction: column;
1844
+ }
1845
+
1846
+ #panel-hema .panel-cuerpo,
1847
+ #panel-bioquim .panel-cuerpo {
1848
+ min-height: 0;
1849
+ }
1850
+ }
1851
+
1852
+ /* RESPONSIVE */
1853
+
1854
+ /* Full HD 1920×1080 */
1855
+ @media (min-width: 1920px) {
1856
+ main {
1857
+ grid-template-columns: 1fr 1fr 449px 399px;
1858
+ }
1859
+
1860
+ #panel-hema,
1861
+ #panel-bioquim,
1862
+ #panel-imagenes {
1863
+ --fs-xs: 10.9px;
1864
+ --fs-sm: 12.5px;
1865
+ --fs-ui: 13.1px;
1866
+ --fs-base: 13.6px;
1867
+ }
1868
+
1869
+ #panel-hema .panel-cuerpo,
1870
+ #panel-bioquim .panel-cuerpo {
1871
+ padding: 0.4rem 12px;
1872
+ overflow-y: auto;
1873
+ }
1874
+
1875
+ #panel-hema .cabecera-columnas span:nth-child(2),
1876
+ #panel-bioquim .cabecera-columnas span:nth-child(2) {
1877
+ width: calc(4.5rem + 1.5rem);
1878
+ }
1879
+
1880
+ #panel-hema .grupo-campo,
1881
+ #panel-bioquim .grupo-campo {
1882
+ margin-bottom: 6px;
1883
+ }
1884
+
1885
+ #panel-hema .titulo-grupo,
1886
+ #panel-bioquim .titulo-grupo {
1887
+ margin-bottom: 0.5rem;
1888
+ padding-bottom: 0.1rem;
1889
+ }
1890
+
1891
+ #panel-hema .fila-campo,
1892
+ #panel-bioquim .fila-campo {
1893
+ margin-bottom: 0.2rem;
1894
+ gap: 0.4rem;
1895
+ }
1896
+
1897
+ #panel-hema .fila-campo input,
1898
+ #panel-bioquim .fila-campo input {
1899
+ padding: 3px 0.4rem;
1900
+ width: calc(4.5rem + 1.5rem);
1901
+ }
1902
+
1903
+ #panel-hema .fila-campo .unidad,
1904
+ #panel-bioquim .fila-campo .unidad {
1905
+ min-width: 3rem;
1906
+ }
1907
+
1908
+ #panel-bioquim .grupo-campo {
1909
+ margin-bottom: 0.2rem;
1910
+ }
1911
+
1912
+ #panel-bioquim .fila-campo {
1913
+ margin-bottom: 2px;
1914
+ }
1915
+
1916
+ #salida-ia {
1917
+ font-size: var(--fs-sm);
1918
+ }
1919
+
1920
+ .pasos-flujo li {
1921
+ padding: 0 20px;
1922
+ }
1923
+
1924
+ .cuerpo-flujo {
1925
+ margin-top: 0;
1926
+ margin-bottom: 0;
1927
+ padding-top: 0.5rem;
1928
+ padding-bottom: 0rem;
1929
+ }
1930
+ }
1931
+
1932
+ @media (max-width: 1100px) {
1933
+ header {
1934
+ display: flex;
1935
+ align-items: center;
1936
+ justify-content: space-between;
1937
+ padding: 0.5rem 1rem;
1938
+ }
1939
+
1940
+ #logo {
1941
+ margin-right: 0;
1942
+ }
1943
+
1944
+ .barra-paciente {
1945
+ display: none;
1946
+ }
1947
+
1948
+ .acciones-cabecera {
1949
+ margin-left: 0;
1950
+ align-self: center;
1951
+ }
1952
+
1953
+ .acciones-cabecera .boton {
1954
+ padding: 6px 12px;
1955
+ font-size: var(--fs-sm);
1956
+ }
1957
+
1958
+ #examenes-subtabs-bar {
1959
+ display: flex;
1960
+ }
1961
+
1962
+ #examenes-subtabs-bar[hidden] {
1963
+ display: none;
1964
+ }
1965
+
1966
+ #panel-hema > .panel-cabecera,
1967
+ #panel-bioquim > .panel-cabecera,
1968
+ #panel-uri > .panel-cabecera,
1969
+ #panel-endo > .panel-cabecera {
1970
+ display: none;
1971
+ }
1972
+
1973
+ .cabecera-col-label {
1974
+ display: none;
1975
+ }
1976
+
1977
+ .cabecera-col-btns {
1978
+ display: flex;
1979
+ gap: 4px;
1980
+ margin-left: auto;
1981
+ }
1982
+
1983
+ .cabecera-columnas--mobile-only .btn-limpiar-panel {
1984
+ margin-left: auto;
1985
+ }
1986
+
1987
+ .cabecera-columnas--mobile-only .btn-importar-pdf {
1988
+ margin-left: 4px;
1989
+ }
1990
+
1991
+ footer {
1992
+ padding: 0;
1993
+ flex-direction: column;
1994
+ }
1995
+
1996
+ #aviso {
1997
+ display: none;
1998
+ }
1999
+
2000
+ .btn-adjuntar-mob {
2001
+ display: flex;
2002
+ }
2003
+
2004
+ #aviso-mob {
2005
+ display: block;
2006
+ }
2007
+
2008
+ #creditos-mob {
2009
+ display: flex;
2010
+ }
2011
+
2012
+ #creditos {
2013
+ display: none;
2014
+ }
2015
+
2016
+ .nav-inferior {
2017
+ display: flex;
2018
+ height: 3.5rem;
2019
+ width: 100%;
2020
+ border-top: none;
2021
+ order: -1;
2022
+ }
2023
+
2024
+ main {
2025
+ display: flex;
2026
+ flex-direction: column;
2027
+ background: var(--surface-page);
2028
+ gap: 0;
2029
+ overflow: hidden;
2030
+ }
2031
+
2032
+ .col3-wrapper {
2033
+ display: contents;
2034
+ }
2035
+
2036
+ main > .panel,
2037
+ .col3-wrapper > .panel {
2038
+ display: none;
2039
+ }
2040
+
2041
+ main > .panel.activo,
2042
+ .col3-wrapper > .panel.activo {
2043
+ display: flex;
2044
+ flex: 1;
2045
+ min-height: 0;
2046
+ overflow-y: auto;
2047
+ }
2048
+
2049
+ main > .panel.activo .subpanel-anim,
2050
+ .col3-wrapper > .panel.activo .subpanel-anim {
2051
+ overflow: visible;
2052
+ }
2053
+
2054
+ #panel-resultados {
2055
+ border-left: none;
2056
+ border-top: none;
2057
+ grid-column: unset;
2058
+ min-height: unset;
2059
+ }
2060
+
2061
+ .cuerpo-flujo {
2062
+ align-items: flex-start;
2063
+ overflow-y: auto;
2064
+ padding: 1rem 20px;
2065
+ margin-top: 0;
2066
+ }
2067
+
2068
+ .pasos-flujo {
2069
+ flex-direction: column;
2070
+ gap: 1rem;
2071
+ }
2072
+
2073
+ .pasos-flujo li {
2074
+ flex-direction: row;
2075
+ text-align: left;
2076
+ align-items: flex-start;
2077
+ gap: 14px;
2078
+ padding: 0;
2079
+ }
2080
+
2081
+ .pasos-flujo li + li::before {
2082
+ display: none;
2083
+ }
2084
+
2085
+ .num-paso {
2086
+ width: 1.5rem;
2087
+ height: 1.5rem;
2088
+ flex-shrink: 0;
2089
+ margin-top: 0.1rem;
2090
+ }
2091
+
2092
+ .pasos-flujo strong,
2093
+ .pasos-flujo p {
2094
+ font-size: var(--fs-sm);
2095
+ }
2096
+
2097
+ .seccion-clinica textarea {
2098
+ flex: 1;
2099
+ min-height: 0;
2100
+ }
2101
+
2102
+ .cuerpo-clinica {
2103
+ flex: 1;
2104
+ min-height: 0;
2105
+ }
2106
+
2107
+ .seccion-clinica .cuerpo-clinica {
2108
+ display: flex;
2109
+ flex-direction: column;
2110
+ }
2111
+
2112
+ .btn-colapsar-subpanel,
2113
+ .btn-colapsar-flujo,
2114
+ .btn-colapsar-patrones {
2115
+ display: flex;
2116
+ }
2117
+
2118
+ .pdf-toast {
2119
+ bottom: 5rem;
2120
+ }
2121
+ }
2122
+
2123
+ /* Indicador de usuario */
2124
+
2125
+ .boton-usuario {
2126
+ font-size: var(--fs-sm);
2127
+ font-weight: 500;
2128
+ padding: 5px 12px;
2129
+ min-height: 32px;
2130
+ border: 1px solid var(--border-1);
2131
+ border-radius: var(--radius-sm);
2132
+ background: transparent;
2133
+ color: var(--text-2);
2134
+ cursor: pointer;
2135
+ max-width: 130px;
2136
+ overflow: hidden;
2137
+ text-overflow: ellipsis;
2138
+ white-space: nowrap;
2139
+ transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
2140
+ }
2141
+
2142
+ .boton-usuario:hover {
2143
+ background: var(--surface-2);
2144
+ color: var(--text-1);
2145
+ border-color: var(--border-2);
2146
+ }
2147
+
2148
+
2149
+ /* Modal overlay */
2150
+
2151
+ .modal-overlay {
2152
+ position: fixed;
2153
+ inset: 0;
2154
+ background: rgba(0, 0, 0, 0.45);
2155
+ z-index: 100;
2156
+ opacity: 0;
2157
+ pointer-events: none;
2158
+ transition: opacity var(--dur-base);
2159
+ }
2160
+
2161
+ .modal-auth.visible ~ .modal-overlay,
2162
+ .modal-overlay:has(+ .modal-auth.visible) {
2163
+ opacity: 1;
2164
+ pointer-events: initial;
2165
+ }
2166
+
2167
+ .modal-overlay.activo {
2168
+ opacity: 1;
2169
+ pointer-events: initial;
2170
+ }
2171
+
2172
+ /* Modal*/
2173
+
2174
+ .modal-auth {
2175
+ position: fixed;
2176
+ top: 50%;
2177
+ left: 50%;
2178
+ transform: translate(-50%, -44%);
2179
+ z-index: 101;
2180
+ width: min(440px, calc(100vw - 2rem));
2181
+ background: var(--surface-1);
2182
+ border: 1px solid var(--border-1);
2183
+ border-radius: var(--radius-md);
2184
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.18);
2185
+ padding: var(--space-6);
2186
+ opacity: 0;
2187
+ transition: opacity var(--dur-base), transform var(--dur-base);
2188
+ }
2189
+
2190
+ .modal-auth.visible {
2191
+ opacity: 1;
2192
+ transform: translate(-50%, -50%);
2193
+ }
2194
+
2195
+ .modal-cerrar svg {
2196
+ width: 16px;
2197
+ height: 16px;
2198
+ fill: currentColor;
2199
+ }
2200
+
2201
+ .modal-cerrar {
2202
+ position: absolute;
2203
+ top: var(--space-3);
2204
+ right: var(--space-3);
2205
+ display: flex;
2206
+ align-items: center;
2207
+ justify-content: center;
2208
+ width: 28px;
2209
+ height: 28px;
2210
+ border: none;
2211
+ border-radius: var(--radius-sm);
2212
+ background: transparent;
2213
+ color: var(--text-4);
2214
+ cursor: pointer;
2215
+ transition: background var(--dur-fast), color var(--dur-fast);
2216
+ }
2217
+
2218
+ .modal-cerrar:hover {
2219
+ background: var(--surface-2);
2220
+ color: var(--text-1);
2221
+ }
2222
+
2223
+ .modal-titulo {
2224
+ font-size: var(--fs-base);
2225
+ font-weight: 600;
2226
+ color: var(--text-1);
2227
+ margin: 0 0 var(--space-4);
2228
+ }
2229
+
2230
+ /*Tabs del modal */
2231
+
2232
+ .modal-tabs {
2233
+ display: flex;
2234
+ gap: 2px;
2235
+ margin-bottom: var(--space-4);
2236
+ border-bottom: 1px solid var(--border-1);
2237
+ padding-bottom: 0;
2238
+ }
2239
+
2240
+ .modal-tab {
2241
+ flex: 1;
2242
+ padding: var(--space-2) var(--space-3);
2243
+ font-size: var(--fs-sm);
2244
+ font-weight: 500;
2245
+ color: var(--text-4);
2246
+ background: transparent;
2247
+ border: none;
2248
+ border-bottom: 2px solid transparent;
2249
+ margin-bottom: -1px;
2250
+ cursor: pointer;
2251
+ transition: color var(--dur-fast), border-color var(--dur-fast);
2252
+ }
2253
+
2254
+ .modal-tab:hover {
2255
+ color: var(--text-2);
2256
+ }
2257
+
2258
+ .modal-tab.activo {
2259
+ color: var(--accent);
2260
+ border-bottom-color: var(--accent);
2261
+ }
2262
+
2263
+ /* Campos del formulario */
2264
+
2265
+ .modal-campo {
2266
+ display: flex;
2267
+ flex-direction: column;
2268
+ gap: 5px;
2269
+ margin-bottom: var(--space-3);
2270
+ }
2271
+
2272
+ .modal-campo label {
2273
+ font-size: var(--fs-sm);
2274
+ font-weight: 500;
2275
+ color: var(--text-2);
2276
+ }
2277
+
2278
+ .modal-campo input {
2279
+ padding: 8px 10px;
2280
+ border: 1px solid var(--border-1);
2281
+ border-radius: var(--radius-sm);
2282
+ background: var(--surface-input);
2283
+ color: var(--text-1);
2284
+ font-size: var(--fs-sm);
2285
+ font-family: var(--font-sans);
2286
+ transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
2287
+ }
2288
+
2289
+ .modal-campo input:focus {
2290
+ outline: none;
2291
+ border-color: var(--border-focus);
2292
+ box-shadow: 0 0 0 3px var(--focus-ring);
2293
+ }
2294
+
2295
+ .modal-campo input.campo-valido {
2296
+ border-color: var(--accent);
2297
+ box-shadow: 0 0 0 3px var(--focus-ring);
2298
+ }
2299
+
2300
+ .modal-campo input.campo-invalido {
2301
+ border-color: var(--alto);
2302
+ box-shadow: 0 0 0 3px rgba(184, 55, 44, 0.12);
2303
+ }
2304
+
2305
+ .aviso-legal-texto {
2306
+ font-size: var(--fs-sm);
2307
+ color: var(--text-3);
2308
+ line-height: 1.4;
2309
+ margin: 0;
2310
+ }
2311
+
2312
+ .aviso-legal-aceptar {
2313
+ display: flex;
2314
+ align-items: center;
2315
+ gap: 8px;
2316
+ margin-top: var(--space-2);
2317
+ }
2318
+
2319
+ .aviso-legal-aceptar label {
2320
+ font-size: var(--fs-sm);
2321
+ font-weight: 500;
2322
+ color: var(--text-2);
2323
+ cursor: pointer;
2324
+ }
2325
+
2326
+ .aviso-legal-aceptar input[type="checkbox"] {
2327
+ width: 16px;
2328
+ height: 16px;
2329
+ min-width: 16px;
2330
+ flex-shrink: 0;
2331
+ padding: 0;
2332
+ border: 1px solid var(--border-1);
2333
+ border-radius: 3px;
2334
+ accent-color: var(--accent);
2335
+ cursor: pointer;
2336
+ }
2337
+
2338
+ .modal-fila-doble {
2339
+ display: grid;
2340
+ grid-template-columns: 1fr 1fr;
2341
+ gap: var(--space-3);
2342
+ }
2343
+
2344
+ .modal-error {
2345
+ font-size: var(--fs-sm);
2346
+ color: var(--alto);
2347
+ margin: 0 0 var(--space-3);
2348
+ min-height: 1.2em;
2349
+ }
2350
+
2351
+ .modal-submit {
2352
+ width: 100%;
2353
+ margin-top: var(--space-2);
2354
+ padding: 10px;
2355
+ font-size: var(--fs-sm);
2356
+ font-weight: 600;
2357
+ justify-content: center;
2358
+ }
2359
+
2360
+ .boton-primario {
2361
+ background: var(--accent);
2362
+ color: var(--text-on-accent);
2363
+ border: none;
2364
+ border-radius: var(--radius-sm);
2365
+ cursor: pointer;
2366
+ transition: background var(--dur-fast);
2367
+ }
2368
+
2369
+ .boton-primario:hover {
2370
+ background: var(--accent-hover);
2371
+ }
2372
+
2373
+ .boton-primario:disabled {
2374
+ opacity: 0.6;
2375
+ cursor: not-allowed;
2376
+ }
2377
+
2378
+ /* ═══════════════════════════════════════
2379
+ MODAL DE PAPERS
2380
+ ═══════════════════════════════════════ */
2381
+
2382
+ .modal-papers {
2383
+ position: fixed;
2384
+ top: 50%;
2385
+ left: 50%;
2386
+ transform: translate(-50%, -44%);
2387
+ z-index: 101;
2388
+ width: min(720px, calc(100vw - 2rem));
2389
+ max-height: calc(100vh - 4rem);
2390
+ background: var(--surface-1);
2391
+ border: 1px solid var(--border-1);
2392
+ border-radius: var(--radius-md);
2393
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.18);
2394
+ display: flex;
2395
+ flex-direction: column;
2396
+ opacity: 0;
2397
+ pointer-events: none;
2398
+ transition: opacity var(--dur-base), transform var(--dur-base);
2399
+ }
2400
+
2401
+ .modal-papers.visible {
2402
+ opacity: 1;
2403
+ pointer-events: initial;
2404
+ transform: translate(-50%, -50%);
2405
+ }
2406
+
2407
+ .modal-papers-cabecera {
2408
+ display: flex;
2409
+ align-items: flex-start;
2410
+ justify-content: space-between;
2411
+ gap: var(--space-3);
2412
+ padding: var(--space-5) var(--space-5) var(--space-3);
2413
+ border-bottom: 1px solid var(--border-1);
2414
+ flex-shrink: 0;
2415
+ }
2416
+
2417
+ .modal-papers-cabecera .modal-titulo {
2418
+ margin: 0;
2419
+ }
2420
+
2421
+ .papers-consulta-label {
2422
+ font-size: 0.78rem;
2423
+ color: var(--text-3);
2424
+ margin: 4px 0 0;
2425
+ }
2426
+
2427
+ .papers-consulta-label span {
2428
+ color: var(--text-accent);
2429
+ font-style: italic;
2430
+ }
2431
+
2432
+ .papers-busqueda {
2433
+ display: flex;
2434
+ gap: var(--space-2);
2435
+ padding: var(--space-3) var(--space-5);
2436
+ border-bottom: 1px solid var(--border-1);
2437
+ }
2438
+
2439
+ .papers-busqueda-input {
2440
+ flex: 1;
2441
+ background: var(--surface-input);
2442
+ border: 1px solid var(--border-1);
2443
+ border-radius: var(--radius-sm);
2444
+ color: var(--text-1);
2445
+ font-family: var(--font-sans);
2446
+ font-size: var(--fs-sm);
2447
+ padding: var(--space-2) var(--space-3);
2448
+ outline: none;
2449
+ transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
2450
+ }
2451
+
2452
+ .papers-busqueda-input:focus {
2453
+ border-color: var(--border-focus);
2454
+ box-shadow: 0 0 0 3px var(--focus-ring);
2455
+ }
2456
+
2457
+ .papers-busqueda-btn {
2458
+ background: var(--accent);
2459
+ border: none;
2460
+ border-radius: var(--radius-sm);
2461
+ color: var(--text-on-accent);
2462
+ cursor: pointer;
2463
+ display: flex;
2464
+ align-items: center;
2465
+ justify-content: center;
2466
+ padding: var(--space-2) var(--space-3);
2467
+ transition: background var(--dur-fast);
2468
+ }
2469
+
2470
+ .papers-busqueda-btn:hover {
2471
+ background: var(--accent-hover);
2472
+ }
2473
+
2474
+ .papers-lista {
2475
+ overflow-y: auto;
2476
+ flex: 1;
2477
+ padding: var(--space-4) var(--space-5);
2478
+ display: flex;
2479
+ flex-direction: column;
2480
+ gap: var(--space-4);
2481
+ scrollbar-width: thin;
2482
+ scrollbar-color: var(--border-2) transparent;
2483
+ }
2484
+
2485
+ .paper-tarjeta {
2486
+ border: 1px solid var(--border-1);
2487
+ border-radius: var(--radius-sm);
2488
+ padding: var(--space-3) var(--space-4);
2489
+ background: var(--surface-2);
2490
+ display: flex;
2491
+ flex-direction: column;
2492
+ gap: 4px;
2493
+ }
2494
+
2495
+ .paper-meta {
2496
+ display: flex;
2497
+ gap: var(--space-2);
2498
+ align-items: center;
2499
+ }
2500
+
2501
+ .paper-anio {
2502
+ font-size: 0.75rem;
2503
+ color: var(--text-3);
2504
+ background: var(--surface-3);
2505
+ border-radius: 4px;
2506
+ padding: 1px 6px;
2507
+ }
2508
+
2509
+ .paper-revista {
2510
+ font-size: 0.75rem;
2511
+ color: var(--text-accent);
2512
+ font-style: italic;
2513
+ }
2514
+
2515
+ .paper-pdf-badge {
2516
+ font-size: 0.7rem;
2517
+ font-weight: 600;
2518
+ color: var(--on-accent, #fff);
2519
+ background: var(--accent);
2520
+ border-radius: 3px;
2521
+ padding: 1px 5px;
2522
+ text-decoration: none;
2523
+ letter-spacing: 0.03em;
2524
+ }
2525
+
2526
+ .paper-titulo {
2527
+ font-size: 0.9rem;
2528
+ font-weight: 600;
2529
+ color: var(--text-1);
2530
+ margin: 0;
2531
+ line-height: 1.4;
2532
+ }
2533
+
2534
+ .paper-titulo a {
2535
+ color: var(--text-accent);
2536
+ text-decoration: none;
2537
+ }
2538
+
2539
+ .paper-titulo a:hover {
2540
+ text-decoration: underline;
2541
+ }
2542
+
2543
+ .paper-autores {
2544
+ font-size: 0.78rem;
2545
+ color: var(--text-3);
2546
+ margin: 0;
2547
+ }
2548
+
2549
+ .paper-resumen {
2550
+ font-size: 0.8rem;
2551
+ color: var(--text-2);
2552
+ margin: 4px 0 0;
2553
+ line-height: 1.55;
2554
+ }
2555
+
2556
+ .papers-cargando,
2557
+ .papers-vacio,
2558
+ .papers-error {
2559
+ text-align: center;
2560
+ padding: var(--space-6) 0;
2561
+ color: var(--text-3);
2562
+ font-size: 0.9rem;
2563
+ }
2564
+
2565
+ .papers-error {
2566
+ color: var(--rojo, #e05);
2567
+ }
2568
+
2569
+ .papers-paginacion {
2570
+ display: flex;
2571
+ justify-content: center;
2572
+ align-items: center;
2573
+ gap: 4px;
2574
+ padding: var(--space-3) var(--space-5) var(--space-4);
2575
+ border-top: 1px solid var(--border-1);
2576
+ flex-shrink: 0;
2577
+ }
2578
+
2579
+ .papers-pag-btn {
2580
+ min-width: 2rem;
2581
+ height: 2rem;
2582
+ padding: 0 var(--space-2);
2583
+ border: 1px solid var(--border-1);
2584
+ border-radius: var(--radius-sm);
2585
+ background: var(--surface-2);
2586
+ color: var(--text-2);
2587
+ font-size: 0.85rem;
2588
+ cursor: pointer;
2589
+ transition: background var(--dur-fast), color var(--dur-fast);
2590
+ }
2591
+
2592
+ .papers-pag-btn:hover:not(:disabled):not(.activo) {
2593
+ background: var(--surface-3);
2594
+ color: var(--text-1);
2595
+ }
2596
+
2597
+ .papers-pag-btn.activo {
2598
+ background: var(--accent);
2599
+ color: var(--on-accent, #fff);
2600
+ border-color: var(--accent);
2601
+ font-weight: 600;
2602
+ }
2603
+
2604
+ .papers-pag-btn:disabled {
2605
+ opacity: 0.35;
2606
+ cursor: not-allowed;
2607
+ }