emi-challenge-fe/desing.md

21 KiB
Raw Blame History

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.

: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)

@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

: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
: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

:root {
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;
  --fw-extrabold: 800; /* Para hero titles */
}

3.4. Line heights

: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

: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:

: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

: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

: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)

: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):

.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

: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

: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

: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:

@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

<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>
.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

/* ❌ 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")

<button class="btn btn--primary btn--lg">Conoce más</button>
.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

<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>
.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

.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

// 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