774 lines
21 KiB
Markdown
774 lines
21 KiB
Markdown
# 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 |