emi-challenge-fe/desing.md

774 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design System — EMI / Falck
> Sistema de diseño derivado de la identidad visual de **EMI (Grupo Falck)**.
> Toda la implementación CSS sigue la metodología **BEM** (`block__element--modifier`)
> y un enfoque **mobile-first responsive**.
---
## 1. Filosofía de diseño
EMI es una marca de **atención médica de urgencia 24/7**. El sistema visual debe transmitir:
| Atributo | Cómo se traduce en UI |
| --------------- | ------------------------------------------------------------------ |
| **Confianza** | Tipografía sólida, alto contraste, espaciado generoso |
| **Urgencia** | Rojo coral como acento dominante en CTAs |
| **Profesional** | Vinotinto profundo (burgundy) como color institucional |
| **Cercanía** | Bordes redondeados (`pill` shape en botones), iconografía amable |
| **Accesible** | Contrastes AA/AAA, áreas táctiles ≥ 44×44px, foco visible |
**Regla de oro**: el rojo coral se usa con moderación, **solo para acciones primarias**. El vinotinto domina secciones institucionales. El blanco respira.
---
## 2. Paleta de colores
### 2.1. Colores de marca (Brand)
| Token | HEX | RGB | Uso |
| ------------------------ | --------- | ---------------- | ---------------------------------------------- |
| `--color-brand-primary` | `#E63946` | `230, 57, 70` | CTAs principales, botones "Conoce más" |
| `--color-brand-primary-hover` | `#C92A3B` | `201, 42, 59` | Hover state del CTA primario |
| `--color-brand-primary-active` | `#A8202F` | `168, 32, 47` | Active/pressed state |
| `--color-brand-secondary` | `#6B1F2A` | `107, 31, 42` | Vinotinto institucional, top bar, fondos |
| `--color-brand-secondary-dark` | `#4D1620` | `77, 22, 32` | Variante oscura del vinotinto |
| `--color-brand-accent` | `#D32F2F` | `211, 47, 47` | Rojo del logo, detalles iconográficos |
### 2.2. Colores de soporte (Support)
| Token | HEX | Uso |
| ---------------------- | --------- | ------------------------------------------------ |
| `--color-success` | `#25D366` | Botón WhatsApp, estados completados |
| `--color-success-dark` | `#1EA952` | Hover del verde |
| `--color-warning` | `#F59E0B` | Alertas no críticas |
| `--color-danger` | `#DC2626` | Errores, eliminaciones, validaciones falladas |
| `--color-info` | `#0EA5E9` | Mensajes informativos |
### 2.3. Neutrales (Scale)
| Token | HEX | Uso típico |
| ------------------ | --------- | ----------------------------------------- |
| `--color-white` | `#FFFFFF` | Fondos de cards, textos sobre vinotinto |
| `--color-gray-50` | `#F9FAFB` | Fondo de página, secciones suaves |
| `--color-gray-100` | `#F3F4F6` | Hovers sutiles, dividers |
| `--color-gray-200` | `#E5E7EB` | Bordes de inputs, separadores |
| `--color-gray-300` | `#D1D5DB` | Bordes activos, placeholders |
| `--color-gray-400` | `#9CA3AF` | Texto deshabilitado, iconos secundarios |
| `--color-gray-500` | `#6B7280` | Texto secundario, labels |
| `--color-gray-600` | `#4B5563` | Texto de cuerpo |
| `--color-gray-700` | `#374151` | Texto principal en fondos claros |
| `--color-gray-800` | `#1F2937` | Headings sobre blanco |
| `--color-gray-900` | `#111827` | Texto de máximo contraste |
### 2.4. Tokens semánticos
> Estos tokens **referencian** los anteriores. Los componentes consumen estos, no los HEX directos.
```css
:root {
/* Backgrounds */
--bg-page: var(--color-gray-50);
--bg-surface: var(--color-white);
--bg-elevated: var(--color-white);
--bg-inverse: var(--color-brand-secondary);
--bg-overlay: rgba(17, 24, 39, 0.6);
/* Text */
--text-primary: var(--color-gray-900);
--text-secondary: var(--color-gray-600);
--text-muted: var(--color-gray-400);
--text-inverse: var(--color-white);
--text-link: var(--color-brand-primary);
--text-link-hover: var(--color-brand-primary-hover);
/* Borders */
--border-subtle: var(--color-gray-200);
--border-default: var(--color-gray-300);
--border-strong: var(--color-gray-400);
--border-focus: var(--color-brand-primary);
/* States */
--state-error: var(--color-danger);
--state-success: var(--color-success);
--state-warning: var(--color-warning);
}
```
### 2.5. Dark mode (opcional, recomendado)
```css
@media (prefers-color-scheme: dark) {
:root {
--bg-page: #0F0A0B;
--bg-surface: #1A1214;
--bg-elevated: #241719;
--text-primary: #F5F5F5;
--text-secondary: #B8B8B8;
--border-subtle: #2D1F22;
/* El rojo y vinotinto se mantienen, son colores de marca */
}
}
```
---
## 3. Tipografía
### 3.1. Stack de fuentes
```css
:root {
/* Display: para titulares grandes (Hero) — peso fuerte, condensada */
--font-display: 'Sora', 'Plus Jakarta Sans', system-ui, sans-serif;
/* Body: legibilidad para párrafos y UI */
--font-body: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
/* Mono: código, números tabulares */
--font-mono: 'JetBrains Mono', 'Fira Code', Menlo, monospace;
}
```
> **Nota**: Sora se usa porque tiene el peso visual robusto que se observa en los titulares de EMI ("Somos atención médica…"). Si se requiere licencia comercial sin Google Fonts, alternativa: **Manrope** o **Plus Jakarta Sans**.
### 3.2. Escala tipográfica (mobile-first, escala fluida)
| Token | Mobile | Desktop | Uso |
| ---------------- | ------ | ------- | ---------------------------------- |
| `--fs-xs` | 12px | 12px | Captions, labels pequeños |
| `--fs-sm` | 14px | 14px | Texto secundario, helper text |
| `--fs-base` | 16px | 16px | Cuerpo de texto (default) |
| `--fs-md` | 18px | 18px | Subtítulos, lead paragraph |
| `--fs-lg` | 20px | 24px | H4, destacados |
| `--fs-xl` | 24px | 32px | H3 |
| `--fs-2xl` | 30px | 40px | H2 (ej: "Nuestros planes") |
| `--fs-3xl` | 36px | 56px | H1 hero (ej: "Somos atención…") |
| `--fs-4xl` | 44px | 72px | Display, posters |
```css
:root {
--fs-xs: 0.75rem;
--fs-sm: 0.875rem;
--fs-base: 1rem;
--fs-md: 1.125rem;
--fs-lg: clamp(1.25rem, 1rem + 1vw, 1.5rem);
--fs-xl: clamp(1.5rem, 1.2rem + 1.5vw, 2rem);
--fs-2xl: clamp(1.875rem, 1.5rem + 2vw, 2.5rem);
--fs-3xl: clamp(2.25rem, 1.8rem + 3vw, 3.5rem);
--fs-4xl: clamp(2.75rem, 2rem + 4vw, 4.5rem);
}
```
### 3.3. Pesos
```css
:root {
--fw-regular: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-bold: 700;
--fw-extrabold: 800; /* Para hero titles */
}
```
### 3.4. Line heights
```css
:root {
--lh-tight: 1.1; /* Headings grandes */
--lh-snug: 1.25; /* Headings medianos */
--lh-normal: 1.5; /* Body */
--lh-relaxed: 1.65; /* Párrafos largos */
}
```
### 3.5. Letter spacing
```css
:root {
--tracking-tighter: -0.04em; /* Hero titles */
--tracking-tight: -0.02em; /* H2-H3 */
--tracking-normal: 0; /* Body */
--tracking-wide: 0.05em; /* Eyebrows, uppercase labels */
}
```
---
## 4. Espaciado (Spacing Scale)
Sistema basado en **múltiplos de 4px**, alineado con buenas prácticas de Material/Tailwind:
```css
:root {
--space-0: 0;
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
--space-24: 6rem; /* 96px */
--space-32: 8rem; /* 128px */
}
```
**Regla de aplicación**:
- Espaciado **dentro** de un componente (padding interno): `--space-2` a `--space-6`.
- Espaciado **entre** componentes: `--space-8` a `--space-16`.
- Espaciado **entre secciones**: `--space-20` a `--space-32`.
---
## 5. Radios de borde
```css
:root {
--radius-none: 0;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-pill: 9999px; /* Botones tipo "Conoce más" */
--radius-full: 50%;
}
```
> **Identidad EMI**: los CTAs primarios usan `--radius-pill` (forma de cápsula). Es un sello distintivo de la marca y debe respetarse.
---
## 6. Sombras
```css
:root {
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
/* Sombra de marca (tinte rojo sutil) para CTAs elevados */
--shadow-brand: 0 10px 25px -5px rgba(230, 57, 70, 0.35);
}
```
---
## 7. Breakpoints responsive (Mobile-first)
```css
:root {
--bp-xs: 360px; /* Mobile pequeño */
--bp-sm: 480px; /* Mobile estándar */
--bp-md: 768px; /* Tablet */
--bp-lg: 1024px; /* Desktop pequeño */
--bp-xl: 1280px; /* Desktop estándar */
--bp-2xl: 1536px; /* Desktop grande */
}
```
### Convención de uso (mobile-first SIEMPRE):
```scss
.task-list {
// Default: mobile
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
// Tablet en adelante
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-6);
}
// Desktop en adelante
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
gap: var(--space-8);
}
}
```
**Regla estricta**: nunca usar `max-width` como base. Siempre escalar **de pequeño a grande**.
### Container widths
```css
:root {
--container-sm: 640px;
--container-md: 768px;
--container-lg: 1024px;
--container-xl: 1200px;
--container-2xl: 1440px;
}
.container {
width: 100%;
max-width: var(--container-xl);
margin-inline: auto;
padding-inline: var(--space-4);
@media (min-width: 768px) {
padding-inline: var(--space-8);
}
}
```
---
## 8. Z-index Scale
```css
:root {
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-popover: 600;
--z-tooltip: 700;
--z-toast: 800;
--z-floating-cta: 900; /* Botón flotante WhatsApp */
}
```
---
## 9. Transiciones y animaciones
```css
:root {
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 400ms cubic-bezier(0.4, 0, 0.2, 1);
/* Easings nombrados */
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
```
**Accesibilidad**: respetar siempre `prefers-reduced-motion`:
```css
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
---
## 10. Metodología BEM — Convenciones obligatorias
### 10.1. Estructura
```
.block { } /* Bloque independiente */
.block__element { } /* Parte interna del bloque */
.block--modifier { } /* Variante del bloque */
.block__element--modifier { } /* Variante de un elemento */
```
### 10.2. Reglas
- **No anidar selectores** en CSS más de 1 nivel (el nombre BEM ya da la jerarquía).
- **No usar selectores de tipo** (`div`, `span`) dentro de un bloque BEM.
- **Nombres en kebab-case**: `task-card__action-button--disabled`.
- **Un componente = un bloque**: si reutilizas algo, conviértelo en su propio bloque.
- **Modifiers booleanos**: `--active`, `--disabled`, `--loading`.
- **Modifiers de valor**: `--size-lg`, `--variant-primary`.
### 10.3. Ejemplo correcto
```html
<article class="task-card task-card--completed">
<header class="task-card__header">
<h3 class="task-card__title">Revisar contratos</h3>
<span class="task-card__badge task-card__badge--success">Completado</span>
</header>
<p class="task-card__description">Lorem ipsum…</p>
<footer class="task-card__footer">
<button class="task-card__action task-card__action--edit">Editar</button>
<button class="task-card__action task-card__action--delete">Eliminar</button>
</footer>
</article>
```
```scss
.task-card {
background-color: var(--bg-surface);
border-radius: var(--radius-lg);
padding: var(--space-4);
box-shadow: var(--shadow-sm);
transition: box-shadow var(--transition-base);
&--completed {
opacity: 0.7;
border-left: 4px solid var(--color-success);
}
&:hover {
box-shadow: var(--shadow-md);
}
}
.task-card__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-3);
}
.task-card__title {
font-family: var(--font-display);
font-size: var(--fs-lg);
font-weight: var(--fw-semibold);
color: var(--text-primary);
margin: 0;
}
.task-card__badge {
font-size: var(--fs-xs);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-pill);
&--success {
background-color: var(--color-success);
color: var(--color-white);
}
}
.task-card__description {
font-size: var(--fs-sm);
color: var(--text-secondary);
line-height: var(--lh-normal);
}
.task-card__footer {
display: flex;
gap: var(--space-2);
margin-top: var(--space-4);
@media (min-width: 768px) {
justify-content: flex-end;
}
}
.task-card__action {
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
cursor: pointer;
transition: background-color var(--transition-fast);
&--edit {
background-color: var(--color-brand-primary);
color: var(--color-white);
&:hover {
background-color: var(--color-brand-primary-hover);
}
}
&--delete {
background-color: transparent;
color: var(--color-danger);
border: 1px solid var(--color-danger);
&:hover {
background-color: var(--color-danger);
color: var(--color-white);
}
}
}
```
### 10.4. Lo que NUNCA debe hacerse
```scss
/* ❌ MAL: anidamiento excesivo */
.task-card {
.header {
.title { ... }
}
}
/* ❌ MAL: selector de tipo */
.task-card h3 { ... }
/* ❌ MAL: doble guion bajo encadenado */
.task-card__header__title { ... }
/* ❌ MAL: modifier sin bloque padre */
.--disabled { ... }
```
---
## 11. Componentes base (referencia de marca EMI)
### 11.1. Botón primario (CTA "Conoce más" / "Afíliate en línea")
```html
<button class="btn btn--primary btn--lg">Conoce más</button>
```
```scss
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-family: var(--font-body);
font-weight: var(--fw-semibold);
border: none;
cursor: pointer;
transition: all var(--transition-base);
text-decoration: none;
min-height: 44px; /* Accesibilidad: área táctil mínima */
&:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Variantes */
&--primary {
background-color: var(--color-brand-primary);
color: var(--color-white);
border-radius: var(--radius-pill);
box-shadow: var(--shadow-brand);
&:hover:not(:disabled) {
background-color: var(--color-brand-primary-hover);
transform: translateY(-1px);
}
}
&--outline {
background-color: transparent;
color: var(--color-brand-primary);
border: 2px solid var(--color-brand-primary);
border-radius: var(--radius-pill);
&:hover:not(:disabled) {
background-color: var(--color-brand-primary);
color: var(--color-white);
}
}
&--whatsapp {
background-color: var(--color-success);
color: var(--color-white);
border-radius: var(--radius-pill);
&:hover:not(:disabled) {
background-color: var(--color-success-dark);
}
}
/* Tamaños */
&--sm { padding: var(--space-2) var(--space-4); font-size: var(--fs-sm); }
&--md { padding: var(--space-3) var(--space-6); font-size: var(--fs-base); }
&--lg { padding: var(--space-4) var(--space-8); font-size: var(--fs-md); }
}
```
### 11.2. Input de formulario
```html
<div class="form-field">
<label class="form-field__label" for="name">Nombre y apellido *</label>
<input class="form-field__input" id="name" type="text" required />
<span class="form-field__error">Este campo es obligatorio</span>
</div>
```
```scss
.form-field {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
&--error {
.form-field__input {
border-color: var(--state-error);
}
}
&__label {
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
color: var(--text-secondary);
}
&__input {
padding: var(--space-3) var(--space-4);
font-size: var(--fs-base);
font-family: var(--font-body);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background-color: var(--bg-surface);
color: var(--text-primary);
transition: border-color var(--transition-fast);
min-height: 44px;
&:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 3px rgba(230, 57, 70, 0.15);
}
}
&__error {
font-size: var(--fs-xs);
color: var(--state-error);
}
}
```
### 11.3. Card
```scss
.card {
background-color: var(--bg-surface);
border-radius: var(--radius-lg);
padding: var(--space-4);
box-shadow: var(--shadow-sm);
@media (min-width: 768px) {
padding: var(--space-6);
}
&--elevated {
box-shadow: var(--shadow-lg);
}
&--bordered {
border: 1px solid var(--border-subtle);
box-shadow: none;
}
&--inverse {
background-color: var(--bg-inverse);
color: var(--text-inverse);
}
}
```
---
## 12. Accesibilidad (no negociable)
- **Contraste mínimo AA**: 4.5:1 para texto normal, 3:1 para texto grande.
- **Foco visible**: usar `:focus-visible`, nunca remover el outline sin reemplazarlo.
- **Áreas táctiles**: mínimo 44×44 px en cualquier elemento interactivo.
- **Roles ARIA**: usar `role`, `aria-label`, `aria-describedby` cuando el elemento no sea semántico nativo.
- **Skip links**: `<a href="#main" class="skip-link">Saltar al contenido</a>`.
- **Lang en el HTML**: `<html lang="es">`.
- **Imágenes**: siempre con `alt` descriptivo o `alt=""` si son decorativas.
---
## 13. Checklist antes de hacer commit
- [ ] Todos los colores vienen de tokens CSS, **nunca hex directos** en componentes.
- [ ] Clases siguen BEM (`block__element--modifier`).
- [ ] Sin selectores anidados más allá de 1 nivel.
- [ ] Mobile-first: el CSS por defecto es para mobile, los `@media` escalan hacia arriba.
- [ ] Botones interactivos tienen `:hover`, `:focus-visible`, `:disabled`.
- [ ] Áreas táctiles ≥ 44px de altura.
- [ ] `prefers-reduced-motion` respetado en animaciones.
- [ ] Contraste verificado con WebAIM Contrast Checker.
- [ ] Layout probado en 360px, 768px, 1024px, 1440px.
- [ ] Sin `!important` salvo en utilidades.
---
## 14. Estructura recomendada de archivos SCSS
```
src/styles/
├── abstracts/
│ ├── _variables.scss # CSS custom properties (tokens)
│ ├── _mixins.scss # Mixins (responsive, focus, etc.)
│ └── _functions.scss
├── base/
│ ├── _reset.scss # Normalize / reset moderno
│ ├── _typography.scss # Estilos base de h1-h6, p, etc.
│ └── _global.scss # html, body, *
├── components/
│ ├── _button.scss
│ ├── _card.scss
│ ├── _form-field.scss
│ └── _task-card.scss
├── layouts/
│ ├── _header.scss
│ ├── _footer.scss
│ └── _container.scss
├── utilities/
│ └── _helpers.scss # .sr-only, .visually-hidden, etc.
└── main.scss # Punto de entrada
```
---
## 15. Mixin útil para media queries
```scss
// abstracts/_mixins.scss
@mixin respond-to($breakpoint) {
@if $breakpoint == sm {
@media (min-width: 480px) { @content; }
} @else if $breakpoint == md {
@media (min-width: 768px) { @content; }
} @else if $breakpoint == lg {
@media (min-width: 1024px) { @content; }
} @else if $breakpoint == xl {
@media (min-width: 1280px) { @content; }
}
}
// Uso:
.task-list {
grid-template-columns: 1fr;
@include respond-to(md) {
grid-template-columns: repeat(2, 1fr);
}
@include respond-to(lg) {
grid-template-columns: repeat(3, 1fr);
}
}
```
---
**Última actualización**: 2026-05-13
**Mantenedor**: equipo de frontend
**Versión**: 1.0.0