Spaces:
Running
Running
Jose Salazar commited on
Commit ·
445de93
1
Parent(s): 178feb4
Deploy Morphos en HF Spaces
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +29 -0
- .gitignore +29 -0
- .hf-skill-manifest.json +0 -0
- .htaccess +39 -0
- .vscode/settings.json +3 -0
- CLAUDE.md +78 -0
- Dockerfile +33 -0
- LICENSE +28 -0
- README.md +227 -9
- SKILL.md +205 -0
- USO_DE_IA.md +69 -0
- api/auth.php +115 -0
- api/conexion.php +49 -0
- api/hf_proxy.php +111 -0
- api/papers_proxy.php +108 -0
- api/setup.php +37 -0
- assets/fonts/Inter/OFL.txt +93 -0
- assets/fonts/Inter/README.txt +118 -0
- assets/fonts/JetBrains_Mono/OFL.txt +93 -0
- assets/fonts/JetBrains_Mono/README.txt +79 -0
- assets/icons/1.svg +1 -0
- assets/icons/2.svg +1 -0
- assets/icons/3.svg +1 -0
- assets/icons/4.svg +1 -0
- assets/icons/5.svg +1 -0
- assets/icons/adjuntar.svg +1 -0
- assets/icons/anterior.svg +1 -0
- assets/icons/busqueda.svg +1 -0
- assets/icons/camara.svg +1 -0
- assets/icons/capturas.svg +1 -0
- assets/icons/citologias.svg +1 -0
- assets/icons/colapsar.svg +1 -0
- assets/icons/dark.svg +1 -0
- assets/icons/examenes.svg +1 -0
- assets/icons/expandir.svg +1 -0
- assets/icons/favicon.ico +0 -0
- assets/icons/flujo.svg +1 -0
- assets/icons/intepretacion.svg +1 -0
- assets/icons/light.svg +1 -0
- assets/icons/login.svg +1 -0
- assets/icons/logo.svg +1 -0
- assets/icons/logout.svg +1 -0
- assets/icons/mail.svg +1 -0
- assets/icons/paciente.svg +1 -0
- assets/icons/papelera.svg +1 -0
- assets/icons/remove.svg +1 -0
- assets/icons/siguiente.svg +1 -0
- assets/lib/pdfjs/pdf.min.js +0 -0
- assets/lib/pdfjs/pdf.worker.min.js +0 -0
- css/styles.css +2607 -0
.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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|