LPDI Actualizado: 09-ago-2026 · Versión 2 · DS — Design System

DESIGN.md — Sistema LPDI · Fuente Única de Verdad de Diseño

Última actualización: 9 de agosto de 2026

Para Claude Code: este documento manda sobre cualquier otra instrucción de diseño, incluida la sección «Diseño visual» de CLAUDE.md (que está DESACTUALIZADA — describe un tema dark slate→green con Plus Jakarta Sans que ya NO existe en producción). Si una regla no está aquí, busca el patrón en el código existente y cópialo. Nunca inventes.

Documento generado por ingeniería inversa del código en producción (eco.lpdi.co, repo lpdi-relacionamiento@main, junio 2026).


⛔ REGLA CERO — No tocar lo ya construido y aprobado

Este documento es DESCRIPTIVO: captura el diseño tal cual está en producción y aprobado por el cliente. NO es una orden de refactorización.

Mantenimiento de este documento (obligatorio)


0. Reglas de oro (lo que más se olvida)

  1. Colores SIEMPRE vía variables CSS (var(--accent), var(--text-primary)…). Nunca hex sueltos, salvo los estados del §4 y el semáforo del §5.
  2. El acento es AZUL #006eff (light) / #4E97FF (dark, navy de marca). No existe ningún tema verde. El verde #22c55e es SOLO para éxito/completado.
  3. Fuentes: Space Grotesk (global, formularios, app) y Outfit (superficies de marca: landing, hub ecosistema, headers de marca). Ninguna otra. Nunca Inter, Roboto, Plus Jakarta Sans.
  4. Íconos: SVG inline estilo stroke (tipo Feather/Lucide), fill="none" stroke="currentColor". Nunca emoji, nunca icon fonts, nunca librerías nuevas.
  5. Todo estilo nuevo debe funcionar en dark mode: usa variables y, si hace falta, override bajo [data-theme="dark"].
  6. Formularios siguen la anatomía del §8 (wrapper 680px, stepper, barra de progreso gamificada, form-card). No inventes layouts nuevos.
  7. Validación por paso con las reglas exactas del §9. No cambies regexes, criterios de contraseña ni textos de error sin pedirlo.
  8. Toda búsqueda por texto es insensible a tildes Y mayúsculas (ver §13). Usa el helper canónico normalizeForSearch en ambos lados de la comparación. Nunca .toLowerCase() solo para una búsqueda del usuario.
  9. Copy en TUTEO (tú), nunca voseo (vos). Todo texto de interfaz usa la segunda persona con «tú»: «puedes», «quieres», «escribe», «confirma», «avisa», «selecciona». PROHIBIDO el voseo argentino («podés», «querés», «escribí», «confirmá», «avisá», «seleccioná»). Esto aplica a labels, placeholders, mensajes de error/advertencia, modales, correos y cualquier microcopy.

1. Stack y dónde viven los estilos


2. Tokens de color (de src/app.css)

Tema claro (default)

Variable Valor Uso
--bg-base #F4F7FB Fondo de página e inputs
--bg-surface #ffffff Cards, modales, headers
--bg-elevated #f8fafc Subgrupos, consent boxes, hover
--accent #006eff Acento principal (botones, links, focus, íconos)
--accent-dim rgba(0,110,255,0.1) Fondos suaves del acento, focus ring
--accent-teal #00ecdd SOLO el «¡Heei!» y detalles de marca
--border #E2E8F0 Bordes 1px–1.5px
--text-primary #003e88 Titulares y texto principal (navy de marca)
--text-secondary #64748b Texto de apoyo
--text-muted #6b7d94 Labels secundarios, notas, placeholders
--badge-soon-bg / --badge-soon-text #E6FFFE / #00b5ae Badge «Próximamente»
--danger #ef4444 Errores, acciones destructivas
--shadow rgba(0,62,136,0.06) Sombras sutiles
--radius-card 14px Radio de cards estándar

Quick cards (4 variantes fondo/borde): blue #eff6ff/#bfdbfe · green #f0fdf4/#bbf7d0 · orange #fff7ed/#fdba74 · purple #faf5ff/#ddd6fe.

Tema oscuro ([data-theme="dark"]) — navy de marca «el fondo del iceberg» (Frank 2026-07-24)

--bg-base #05122E · --bg-surface #102C5A · --bg-elevated #173A6E · --accent #4E97FF · --accent-dim rgba(78,151,255,0.16) · --accent-teal #00ECDD · --border rgba(160,195,255,0.16) · --text-primary #F1F6FF · --text-secondary #C4D3EC · --text-muted #93A6C8 · --danger #FF6B78 · --danger-strong #FF8591 · --shadow rgba(0,0,0,0.45) · badge-soon #0d3d3a/#00ECDD. Quick cards re-derivadas sobre navy.

--danger-strong es un token semántico: en CLARO vale #c0392b (el rojo fuerte fijo de §15.1/§15.2, idéntico al histórico) y solo se eleva en oscuro. El shell del dashboard suma un degradado radial navy (.app-shell) y vidrio (backdrop-filter) en las barras del marco.

El toggle de tema guarda en localStorage y aplica data-theme en <html> (vía app.html, en todas las rutas).

Política de modo oscuro (REGLA DEL SISTEMA — Frank 2026-07-24)

Colores de marca fijos (no variables — solo superficies de marca)


3. Tipografía

Contexto Fuente Notas
Global / app / formularios / dashboard Space Grotesk (300–700, Google Fonts, importada en app.css) body y h1–h6 (font-weight: 700 en headings)
Landing /, hub /ecosistema, FormBrandHeader Outfit (300–900, cargada por página en <svelte:head>) Marca: pesos 700–900

Escala observada (no inventar tamaños nuevos sin razón): - «¡Heei!»: clamp(52px, 8vw, 72px) (landing) / clamp(40px,7vw,56px) (login), peso 900, letter-spacing −2/−3px, color teal. - H1 hero: clamp(18px, 3.5vw, 28px), 700, navy, letter-spacing: -0.6px. - Títulos de card: 19px/800. Título de sección de formulario: 18px/700 centrado. - Cuerpo/descripcion: 13–14px. Labels de campo: 13px/600. Labels uppercase (login): 11px/600, letter-spacing: 0.5px. - Microcopy/notas: 11–12px. Badge soon: 9px/500.


4. Colores de estado

Estado Color Patrón
Éxito #22c55e mensajes, criterios cumplidos, steps completados, bg rgba(34,197,94,0.1)
Error #ef4444 bordes de input, asterisco .required, textos; banner claro: bg #fef2f2, borde #fca5a5, texto #b91c1c; dark: rgba(239,68,68,0.1) / #fca5a5
Advertencia / hint ámbar: bg #fffbeb, borde #fcd34d hints de logo/draft
Info var(--accent-dim) + var(--accent) mensajes informativos
Email existente bg #f0fdf4, texto #166534, borde #bbf7d0 .email-check-status.exists
Email nuevo bg #eff6ff, texto #1e40af, borde #bfdbfe .email-check-status.new-account

5. Escala de scores — 4 bandas (FUENTE ÚNICA: src/lib/utils/scoreColor.ts)

Unificada por Frank (2026-07-02): antes coexistían un semáforo simple de 3 bandas (34/67) y la escala de 4 bandas de los formularios. Ahora TODO usa las 4 bandas (scoreColor() + scoreMessage()):

 0–39 %  → #ef4444 rojo    · «Tu perfil tiene baja visibilidad.»
40–69 %  → #f97316 naranja · «Faltan datos clave para hacer match.»
70–89 %  → #f59e0b ámbar   · «¡Casi listo! Estás en el radar.»
90–100 % → #22c55e verde   · «Perfil destacado. Alta probabilidad de match.»

Usado en barras de completitud de perfil (dropdown del hub, sidebar, formularios FS/FI/FD, correos). Importar siempre scoreColor()/scoreMessage(), nunca re-implementar los umbrales (40/70/90).


6. Íconos


7. Componentes canónicos

Botones

Cards de módulo (landing/hub)

rgba(255,255,255,0.9) + backdrop-filter: blur(8px), borde 1.5px solid #E2E8F0, radius 18px, padding 26px, gap 14px; hover: borde #006eff, sombra 0 10px 36px rgba(0,110,255,0.1), translateY(-2px). - card-icon: 52×52, radius 13px, linear-gradient(135deg,#EBF3FF,#D6E8FF), SVG 28px stroke #006eff. - card-title 19px/800 navy · card-desc 13px #5a7aa8. - Tags pill: padding 2px 11px, radius 100px, 11px/600, bg #EBF3FF, texto #006eff, borde #C7DCFF. - Card deshabilitada: clase soon + .badge-soon + botones .subsystem-btn--soon; módulos con login requerido llevan .lock-badge (candado).

Badges

Inputs

Opciones tipo card (radios y checkboxes)

Stepper y progreso (formularios)

Modales

⚠️ Reemplazado por «Estándares V.2 → Pop-ups / Modales del Sistema». El estándar canónico vigente es: overlay rgba(0,0,0,0.55) con z-index:200, caja radius 14px, sombra 0 12px 32px rgba(0,0,0,0.25), variante centrada de confirmación y doble confirmación destructiva. Ver esa sección. (Se conserva la nota del modal de éxito: check verde + countdown «Te llevamos al ecosistema en Xs».)

Indicadores


8. Patrones de página

Header público (landing y hub)

Sticky, max-width 860px, bg rgba(255,255,255,0.92) + backdrop-filter: blur(12px), borde inferior #E2E8F0, radius solo abajo 0 0 14px 14px, sombra 0 1px 4px rgba(0,62,136,0.06). Logo 34px a la izquierda. Derecha: «Iniciar sesión» outline (anónimo) o botón Dashboard + badge de entidad + avatar 36px (borde 2px #006eff, placeholder bg #e8f0ff) con dropdown (perfiles con mini barras de progreso scoreColor).

Hero de marca

¡Heei! teal gigante → H1 navy con <strong> azul → sub gris azulado. Página entra con fade+slide (opacity 0 / translateY(12px)mounted). Después: label de sección («¿Qué quieres hacer hoy?», 12.5px #94A3B8) y grid de module-cards (2 o 3 columnas, 1 en <600px). Métricas en metrics-bar (número grande + label, separadores verticales), formato toLocaleString('es').

Hub Central (/ecosistema) — estados y cards bloqueadas

Tres estados: anónimo (sin login), general (logueado sin entidad), enabled (logueado + Startup/ICG registrada). Copy: saludo «¡Heei {nombre}!», H1 «Tu lugar en el ecosistema» (general/enabled), label de sección «¿Qué quieres hacer hoy?». Cards bloqueadas (Matchmaking y Networking en estado general): contenido atenuado, card completa clickeable, badge 🔒 circular ámbar arriba-derecha (no overlay que tape la card). Al hacer click → modal central «[Módulo] no disponible aún» con texto «Completa tu registro como Startup/PyME o como ICG para desbloquear este módulo» + botón primario «Soy Startup / PyME» → /registro-startup, botón secundario «Soy ICG» → /registro-icg, link «Cancelar». (Decisión aprobada por Frank — cerrada, no reabrir. Consolidada aquí desde la antigua BIBLIA Hub Central 2026-07-05.)

Header de formularios (FormBrandHeader)

Card blanca sticky radius 12px: logo 36px · centro «LPDI · Ecosistema de Innovación» (Outfit 14px/500, oculto en <640px) · derecha link azul con globo «Ir al ecosistema».

Layout de formulario

page-wrapper max-width 680px centrado. Orden: FormBrandHeader → header centrado (badge pill uppercase con --accent-dim + título 17px/700 + subtítulo) → meta de draft → stepper → barra de progreso → .form-card (radius 16px, padding 32px 28px, sombra suave) → .btn-row (Atrás secundario / Siguiente primario). Two-col grid para pares de campos (colapsa <480px).

Dashboard

Sidebar con items radius 8px (texto #003e88, hover/activo --accent-dim), avatar/logo con linear-gradient(135deg, var(--text-primary), var(--accent)), mini barras de progreso 4px con scoreColor, acciones peligrosas en #ef4444, popups radius 10px con sombra 0 8px 24px rgba(0,0,0,0.15).

Centrado, 13px --text-muted, borde superior: «© 2026 La Punta del Iceberg 📍(pin SVG) Hecho en LATAM — Todos los derechos reservados» + links legales 12px (Términos · Tratamiento de Datos · «Ayúdanos a mejorar» → /ideas).


9. Formularios: validación y comportamiento (NO CAMBIAR sin aprobación)

Reglas universales

Regexes canónicas

Email:    /^[^\s@]+@[^\s@]+\.[^\s@]+$/
LinkedIn: /^https?:\/\/(www\.)?linkedin\.com\/(in|company)\//i
Website:  el input quita /^https?:\/\//, el valor final es `https://${dominio}`

Contraseña (5 criterios, checklist en vivo)

Mínimo 8 caracteres · una mayúscula · una minúscula · un número · un carácter especial. Checklist en grid 2 columnas con /; cumplido → verde #22c55e, incumplido (con texto presente) → rojo. Confirmación con borde verde/rojo según coincidencia + hint «✓/✗ Las contraseñas (no) coinciden». El submit se bloquea (disabled) hasta cumplir todo + aceptar T&C y Tratamiento de Datos.

Consentimiento TYC / Tratamiento de Datos (PDP) — regla de sistema

La aceptación de Términos y Condiciones (TYC) y de la Política de Tratamiento de Datos Personales (PDP) SIEMPRE es explícita: casillas de verificación separadas que el usuario marca por su cuenta. Nunca implícita, nunca preseleccionada, y nunca agrupada con otra acción en un solo botón. En particular, al crear una cuenta nueva mientras se acepta una transferencia de entidad, no se combina «aceptar transferencia + crear cuenta + TYC/PDP» en un único clic: TYC y PDP son casillas propias que deben marcarse antes de habilitar el submit. Aplica a todo alta de cuenta (registro público y cuenta creada al aceptar una transferencia). (Regla confirmada por Frank 2026-07-05.)

Checks asíncronos (debounce 600ms)

Lógica condicional clave (FS startups)

Drafts y autosave (LAP-19)

Score gamificado

Cada formulario calcula 0–100 pts con pesos por campo (ver scoreFS en el shell); la barra usa scoreColor. Si tocas campos, recalibra los pesos para que sigan sumando 100.


10. Voz y copy


11. Anti-patrones (PROHIBIDO)

  1. ❌ Tema verde, gradients slate→green, Plus Jakarta Sans (CLAUDE.md viejo — ignorar esa sección).
  2. ❌ Hex hardcodeados para acento/texto/fondos (usa variables). El azul #2563eb que aparece en fallbacks viejos NO es canónico: el acento es #006eff/#3b82f6.
  3. ❌ Emoji como íconos, icon fonts, o instalar librerías de íconos.
  4. ❌ Inventar variantes de botón, badge «Próximamente» alternativo, radios de esquina nuevos o sombras dramáticas.
  5. ❌ Estilos que solo funcionan en tema claro (probar dark con data-theme="dark").
  6. ❌ Cambiar regexes, criterios de contraseña, umbrales del semáforo (40/70/90) o pesos de score sin instrucción explícita.
  7. ❌ Persistir contraseñas o aceptación de términos en drafts/localStorage.
  8. ❌ Teal #00ecdd para texto largo o botones — es un destello de marca, no un color de UI.
  9. ❌ Layouts de formulario fuera del patrón 680px + stepper + form-card.
  10. ❌ Copy en inglés, trato de «usted», o clichés de emprendimiento (cohetes/apretones de manos en imágenes).

12. Checklist antes de commitear UI

Estándares de formularios — alineación FP ↔ FD/FI (Frank 2026-06-22)

Todos los formularios (FP perks, FD startup, FI ICG, FE evento, perk externo) comparten estos estándares. NO desviarse al crear formularios nuevos — Frank pidió explícitamente "que no vuelva a pasar a futuro".

Mnemotécnica de formularios (actualizada Frank 2026-06-30): | Sigla | Formulario | Ruta | Notas | |-------|-----------|------|-------| | FS | Startup Simplificado | /registro-startup (público) · /dashboard/startups/nuevo | crea startups.simplified_complete=true | | FD | Startup Detallado | /registro-startup-detallado · /dashboard/startups/nuevo-detallado | completa el mismo registro | | FIS | ICG Simplificado | /registro-icg (público) · /dashboard/icg/nuevo-simplificado | crea icgs.simplified_complete=true | | FID | ICG Detallado | /dashboard/icg/nuevo (solo dashboard) | completa el mismo registro (era «el registro de ICG») | | FI | paraguas de los dos formularios de ICG (FIS + FID) | — | cuando el estándar aplica a ambos | | FUS | Usuario Simplificado (registro público de usuario) | /registro-usuario (público) | crea la cuenta de usuario + PU | | FUD | Usuario Detallado | /dashboard/perfil-usuario/editar | edita el perfil de usuario (PU) | | FU | paraguas de los formularios de usuario (FUS + FUD) | — | · PU = Perfil de Usuario (cuenta) | | FP | Perks (beneficios) | — | · FE = Evento |

Relación FS↔FD y FIS↔FID: misma entidad/tabla; el simplificado la crea, el detallado la enriquece (no la pisa). El link público de ICG (/registro-icg) lleva obligatoriamente al FIS; el FID solo se alcanza desde el dashboard. El correo del FIS no es el contacto del FID (el FID trae contacto separado), excepto la categoría Inversionista Individual, donde el registrante ES el contacto.

REGLA — sesión post-registro (Frank 2026-07-01): todo formulario público que cree una cuenta de usuario debe iniciar sesión del cliente tras registrar (signInWithPassword con las credenciales recién creadas) para que el usuario caiga loggeado en /ecosistema, no en un estado sin sesión. Aplica a FUS, FS, FIS, FE y cualquier formulario público nuevo. (El registro server-side crea la cuenta pero NO establece sesión de navegador por sí solo.)


Estándares V.2 (Frank 2026-06-23) — pop-ups, encabezados, correos, papelera

Cierra los vacíos que causaron reprocesos. Estos patrones son CANÓNICOS; no crear variantes nuevas.

Pop-ups / Modales del Sistema (unificado — reemplaza «7. Modales»)

Encabezado de formulario (.form-top) — alineado a FD/FI

Títulos de panel y subsecciones

Panel de Revisión (perks FP/externo)

.rev-hero (wrapper degradado linear-gradient(135deg,var(--blue-50),#eef2fb) + tarjeta blanca: logo + pill de categoría arriba, nombre 20px navy, frase de valor, CTA azul a la derecha) → .rev-sections/.rev-sec (bg --surface-sunken + borde, cabecera con «Editar» + divisor inferior, filas .rev-kv con divisores) → nota centrada. Una sola barra de progreso (la global del formulario); NO duplicar dentro del panel.

Tarjetas de resumen clicleables (stat-strip) — patrón general

REGLA (Frank 2026-06-24): en TODO panel con stat-strip, cada .stat es un <button> que filtra el listado por su estado; la card activa se resalta con .stat.active (borde var(--accent) + sombra 0 2px 12px rgba(0,110,255,.15)); hover sube translateY(-1px) + borde accent. La primera card («Total / Beneficios / Eventos totales») limpia el filtro. Las cards cubren los estados reales del módulo (no un subconjunto arbitrario). Los conteos del stat-strip son globales y estables (sobre todos los registros, no la lista filtrada) para que los números no salten al filtrar desde las propias cards — calcular server-side sin el filtro de status. Aplicado en: dashboard/beneficios (aliado), dashboard/beneficios/admin, dashboard/eventos/admin.

Sistema de correos (FP/FE) — src/lib/server/notification-emails.ts

Diseño aprobado del REPORTE de inventario de correos (Frank 2026-07-02)

Archivo canónico: playgrounds.digitalhubassist.ai/pmo/informe-emails-lpdi-completo.html (fuente en ~/playgrounds/pmo/). Este es el ÚNICO formato válido del reporte — no usar variantes viejas (p. ej. inventario-correos-lpdi-rev9.html, tema oscuro). Font DM Sans, fondo #f4f4f5, título navy #003e88. - Estructura: cabecera (título + subtítulo + bloque de taxonomía) → índice/TOC (table.idx-table agrupado por categoría, con dot de color + conteo) → tarjetas por categoría. - Categorías y su color (borde/acento del .card-header y .badge-num): USUARIO #6366f1 · STARTUP #16a34a · ICG #ea580c · CRON #7c3aed · FP #006eff · FE #0891b2. - Cada tarjeta (div.card id=C<NN>): .card-header {categoría} con .badge-num (número oficial) + .tag-new «NUEVO» (opcional) + .card-title; luego table.field-table con las filas Nombre técnico (tax-code), Función en código, Asunto, Trigger, Destinatario, CTA, Descripción, TyC / Tratamiento; y al final div.preview-section con <img class="preview-img"> al render emails/EMAIL-<NN>.png?v=N. - Taxonomía del nombre técnico: CATEGORÍA / DESTINATARIO / EXISTENCIA / PRIVILEGIO / TIPO MENSAJE / TYC / FUENTE (p. ej. USUARIO/USER/AMBOS/NO-ED/Actualización T&C y Política/NO-TYC/LPDI). - Al agregar un correo: card en su categoría + fila en el TOC + preview embebido en emails/ + actualizar los conteos (por categoría y total). El número oficial es global y secuencial (siguiente libre). - Conteos = correos ACTIVOS (los 3 eliminados van aparte, atenuados con opacity:0.5;border:2px dashed). La suma de la leyenda por categoría debe igualar «N correos activos». Nombres de categoría en la leyenda: USUARIO · STARTUP · ICG · CRON · PERKS · EVENTOS (Frank 2026-07-02; antes «FP»/«FE»). - Labels de versión (Frank 2026-07-02): .tag-new «NUEVO» = correo nuevo frente a la versión anterior del inventario; .tag-mod «MODIFICADO» = correo modificado frente a la versión anterior. Se recalculan en cada bump de versión (limpiar los pegados): solo lo agregado/cambiado desde la versión previa lleva label. Bumpear Versión N + fecha en el header al publicar cambios. - Cada ficha trae un enlace .back-to-index («↑ índice» → #indice) en su .card-header para volver al índice.

Papelera / soft-delete (espejo del borrado de entidades)

Analítica (GA4)

gtag.js global en src/app.html (ID G-J38PDEV5MC). Eventos de conversión sign_up_startup / sign_up_icg al crear la entidad (el server devuelve created:true en el INSERT y el cliente dispara trackSignUp). La medición mejorada cubre la navegación SPA.


Estándares V.3 (Frank 2026-06-23) — paneles admin, Mis beneficios, canje de usuarios no registrados, Referidos

Documenta lo construido en el módulo Beneficios que aún no estaba en el DS. Patrones CANÓNICOS; al crear paneles/páginas nuevas del mismo tipo, espejarlos.

Página «Mis beneficios» (aliado) — dashboard/beneficios

Panel admin de Beneficios — dashboard/beneficios/admin

Pantalla propia de Referidos (ReferidosPanel.svelte + rutas dedicadas)

REGLA (Frank 2026-06-23): los listados largos NO van en pop-up/modal NI mezclados en línea con otra vista — van en su PANTALLA PROPIA (ruta dedicada del dashboard). Los modales se reservan para confirmaciones/avisos cortos. - Rutas: aliado → dashboard/beneficios/referidos (gateada por sesión + canAccessPerks); admin → dashboard/beneficios/admin/referidos (gateada por is_perk_admin). Cada una usa la superficie estándar .perks-surface + .dash-head (título «Mis referidos» / «Referidos (admin)» + subtítulo + botón Volver .btn-secondary). - Accesos: ítem «Mis referidos» en el sidebar (sección Beneficios, {#if canPublishPerks}) + enlace en la cabecera de Mis beneficios; en admin, enlace «Referidos» en la cabecera del panel admin. - Componente ReferidosPanel.svelte (prop admin): tarjeta .ref-section (NO overlay, NO position:fixed) con chip de conteo + búsqueda + [filtros admin] + tabla. NO repite título (lo provee la página). - Búsqueda por texto (.form-input) siempre visible: filtra al instante por nombre/correo/beneficio/aliado (cliente). - Modo admin: filtros (.form-select): aliado / tipo de entidad / categoría / tipo de canje + rango de fechas + Limpiar (.btn-secondary). Validación de rango: «Hasta» < «Desde» marca los campos con .input-error (marco rojo del DS) + mensaje, y no aplica el filtro inválido. - Fechas: patrón canónico del DS — input de texto dd-mmm-aaaa + botón calendario (showPicker sobre un type=date oculto). La columna de fecha de la tabla también va en dd-mmm-aaaa. NUNCA type=date nativo visible. - Fecha de apertura ≥ hoy (Frank #32, 2026-07-29) — REGLA DEL SISTEMA. La fecha de apertura/inicio de vigencia de un beneficio (FPI y FPE) no puede ser anterior a hoy. Doble barrera: (1) el type=date oculto lleva min = hoy (YYYY-MM-DD) para que el calendario ni siquiera permita elegir una fecha previa; (2) validación en el submit que bloquea e informa el error. Al editar un beneficio ya abierto, el min NO aplica (se respeta la fecha de apertura original que pudo quedar en el pasado). Patrón replicable a cualquier fecha «no puede ser en el pasado». - Tabla .ref-table: Nombre (navy/600) · Correo (mailto azul) · Teléfono · Beneficio · [Aliado en admin] · Tipo de canje (badge pill) · Fecha de solicitud. Estados de carga (spinner) y vacío (ícono usuarios). - Datos desde perk_redemptions (tabla append-only, F5) + profiles (nombre/correo/teléfono) vía endpoint GET /api/perks/referidos (?admin=1 gateado). El teléfono prioriza lead_phone del canje; si no, profiles.phone.

Lógica de canje — usuarios NO registrados / no-aliados (RedeemFlow.svelte, F5)

Comportamiento de un usuario del ecosistema (o visitante) frente a un beneficio publicado, en perks/[id]. - Gate de autenticación: si el usuario NO está autenticado, al pulsar el CTA de canje se redirige a /auth/login?redirectTo=<ruta actual>. La ficha pública también ofrece «Crear mi cuenta gratis» → /registro-usuario. El dueño del perk no puede canjear el suyo (isOwner en cliente + bloqueo server-side). - 4 ramas según redemption (componente único, render condicional): - code → muestra código + enlace (fase success-code). - link → entrega enlace y lo abre en pestaña nueva (success-link). - schedule → entrega URL de agenda y la abre en pestaña nueva (success-schedule). - leadformulario de lead (fase lead-form): teléfono + casilla de consentimiento explícito obligatoria («para que el aliado te contacte»); sin ambos, el server rechaza. Al enviar, manda email al aliado y registra el canje (success-lead). - Fases: idle → [lead-form] → submitting → success-* | error. Todo canje (las 4 ramas) escribe una fila en perk_redemptions (trazabilidad inmutable: sin UPDATE/DELETE); el de tipo lead además guarda lead_phone/lead_consent y marca failed si el email no sale. Incrementa clicks del perk (best-effort). - El registro de canjes alimenta el panel Referidos (arriba).


13. Búsqueda por texto — insensible a tildes y mayúsculas (estándar, Frank 2026-06-24)

REGLA DURA: ningún buscador del Sistema es sensible a tildes ni a mayúsculas. «tecnologia» debe matchear «Tecnología», «bogota» → «Bogotá», «MEDELLIN» → «Medellín».

14. Panel de aprobación de eventos (dashboard/eventos/admin/aprobacion)

Pantalla aparte del panel resumen (/admin, que queda como vista compacta). Gateada is_event_admin. Tabla ANCHA con scroll horizontal (.apr-table width:max-content; min-width:100%), una fila por evento. Columnas = TODOS los campos del formulario de registro en el MISMO ORDEN de los paneles (1 Contacto → 2 Datos clave → 2B Ubicación → 3 Temática → 4 Registro) + trazabilidad (creado por/correo + fecha, aprobado por/correo + fecha, editado por + fecha; eliminado por va en la papelera del resumen). Encabezados clicleables ordenan por columna; la columna Acciones es position:sticky; right:0. Acciones por fila: Aprobar (PATCH aprobar) · Editar (ficha del evento) · Rechazar (modal DS con feedback obligatorio, PATCH rechazar) · Eliminar (doble confirmación DS, DELETE → papelera). Búsqueda insensible a tildes (§13) + descarga CSV de las filas filtradas. Reutiliza los endpoints existentes; no introduce escrituras nuevas.


15. Estandarización de paneles (Frank 2026-06-30)

Estos estándares aplican a todos los paneles de admin y del usuario (Administrar eventos, Administrar beneficios, Supereventos, Mis eventos, Mis beneficios, Mis referidos) para que se vean como un solo sistema. Reemplazan cualquier guía anterior que contradiga estos puntos.

15.1 Íconos de acción (CTAs) — estilo «soft»

Las acciones por fila/card son botones de ícono (no texto), estilo «soft» (Tailwind UI / Stripe / Linear): ícono de color sobre fondo tintado del mismo color al ~10–12 % con borde al ~22 %. NUNCA ícono blanco sobre color pleno (queda cargado), NUNCA outline plano gris (se lee confuso). - Tamaño 30–32 px, radio 8 px. .icon-btn + variante semántica. - El color comunica el TIPO de acción, no el módulo (mismo color = mismo significado en todos): - solid-go verde #15904f — Aprobar / Aprobar y publicar / Publicar / Aceptar - solid-info azul #006eff — Reactivar / Editar / Restaurar - solid-warn ámbar #ea580c — Solicitar cambios / Desactivar / Rechazar - solid-danger rojo #c0392b — Finalizar / Eliminar - solid-neutral gris #5b6b80 — Duplicar / Ver / Re-asociar / Destacar - Cada botón lleva aria-label + title con el nombre de la acción (el ícono no basta para lectores). - Catálogo oficial de íconos (ícono · color · función · panel): playgrounds.digitalhubassist.ai/lpdi-iconos-ds.html.

15.2 Cards de resumen (stat-strip) — criterio de color ÚNICO

Reemplaza el color-por-estado anterior. Los NÚMEROS de las cards usan un solo criterio en TODOS los paneles: - Navy de marca #003E88 por defecto. - Azul #006eff cuando la card está seleccionada (filtro activo). - Rojo #c0392b solo en «Eliminados». - El label siempre en MAYÚSCULA, font-size:11.5px; font-weight:600; letter-spacing:.02em; color:text-muted. - Grid repeat(auto-fit, minmax(116px, 1fr)) (envuelve en varias filas; no fijar N columnas).

15.3 Títulos de panel + pill (eyebrow)

15.4 Estado vacío unificado (EmptyState.svelte)

Todo listado/tabla usa el componente src/lib/components/EmptyState.svelte (card con borde + ícono en cuadro tintado + título navy + subtítulo + acción opcional). Props: title, subtitle, actionLabel, onAction. PROHIBIDO el texto plano suelto como estado vacío. Acción típica: «Ver todos» / «Limpiar filtros» / crear.

15.5 Ancho de paneles

15.6 Doble confirmación

Acciones sensibles (desactivar, finalizar, reactivar, restaurar, duplicar, eliminar) usan modal con checkbox de confirmación; la 1ª confirmación es abrir el modal, la 2ª es marcar el checkbox. El texto del checkbox es específico por acción («Confirmo que quiero finalizar este beneficio»), nunca genérico ni heredado de otra acción.

15.7 Máquina de estados de Beneficios

borrador → enviado → aceptado → publicado; más inactivo y finalizado. - Inactivo = cierre manual anticipado: se Reactiva (no se edita ni elimina). Al reactivar vuelve a Publicado (vigencia iniciada) o Aprobado (vigencia futura; el cron lo publica en la fecha). - Finalizado = cierre definitivo (manual o automático por vigencia vencida, vía cron publish-scheduled): solo lectura, no editable ni eliminable (salvo Superadmin), se puede Duplicar. - Publicado = visible para todos (vigencia ya inició). Aprobado = aprobado pero aún no inicia por fechas. - Cards en orden: Todos · Publicados · Aprobados · Pendientes · Cambios por aprobar · Finalizados · Inactivos · Borradores · Eliminados.

15.8 Módulo de Permisos (dashboard/admin/permisos)

Matriz usuario × módulo con roles: admin, curador, editor_confianza, duplicador. Solo super_admin gestiona. El flag Duplicar (duplicador) habilita duplicar registros de otros; el dueño duplica lo suyo siempre, el superadmin y el admin de módulo también (can_duplicate = super OR rol admin OR rol duplicador en el módulo). El curador lo tiene OFF por defecto.

15.9 Sidebar — comportamiento por defecto

Al entrar al Dashboard se despliegan solo las secciones con opciones activas (Mis Startups/PyMEs, Mis ICG, Contenidos, Beneficios, Administración); las secciones 100 % «Próximamente» (Explorar el ecosistema, Matchmaking, Networking, Newsletters) arrancan plegadas. Dentro de una sección activa, las subsecciones con opciones activas van desplegadas (Eventos) y las 100 % inactivas plegadas (Convocatorias, Noticias). El estado se persiste en localStorage (lpdi-sidebar-sections-v2). Orden de secciones: Explorar el ecosistema · Mis Startups/PyMEs · Mis ICG · Contenidos · Beneficios · Matchmaking · Networking · Newsletters · Administración.

15.10 Modal de edición (plantilla — EventEditModal.svelte)

Patrón canónico del modal de edición desde un panel de admin. Es la plantilla para los futuros modales de edición de Convocatorias y Noticias. Estructura (.eem-*): - .eem-overlay + .eem-modal (dialog, aria-modal), .eem-head con <h2> («Editar [entidad]») + subtítulo con el código (.eem-sub) + botón cerrar (.eem-close). - .eem-body: el formulario completo pre-llenado, en el mismo orden de paneles que el formulario de registro (para eventos: 1 Contacto → 2 Datos clave → 2B Ubicación → 3 Temática → 4 Registro). - Solo lectura para registros finalizados: readOnly cuando el registro ya terminó (salvo Superadmin, prop canEditFinalizado). Se aplica el atributo inert a .eem-body (desactiva TODA edición y foco) + banner .eem-readonly-banner («🔒 … solo lectura»). - Validación al guardar (triedSave): asteriscos * en obligatorios, resaltado rojo .input-error en los campos faltantes, y banner de errores bloqueante .eem-error-banner («Corrige lo siguiente antes de guardar») que impide el guardado hasta resolver (obligatorios, incongruencias de país/provincia/zona horaria, speakers incompletos). Los avisos no bloqueantes van en .eem-warn-banner. - Doble control de seguridad: confirmación al guardar y aviso al cerrar sin guardar si hay cambios. - Al guardar con éxito, el panel refresca la fila (no recarga la página).


16. Catálogo público de beneficios y ficha del aliado (/perks, /perks/[id])

Cara pública del módulo Beneficios (accesible a toda la comunidad). Superficie .perks-surface con los tokens de marca.

16.1 Catálogo /perks

16.2 Ficha del aliado /perks/[id]

16.3 Restyle de presentación del catálogo y la ficha (Frank 2026-07-08) — SOLO diseño

Ajustes visuales del catálogo/ficha públicos. NO se tocó backend, endpoints, RLS ni el flujo de canje.

⚠️ GOTCHA CRÍTICO — colisión .outline con Tailwind. La clase modificadora del botón se llama outline (class="btnL outline") y choca con la utilidad de Tailwind .outline { outline-style: solid }. Con el color de texto azul (currentColor) y el ancho medium (3px) del navegador, eso dibujaba un anillo outline de 3px azul sólido alrededor del botón, que se leía como un «borde grueso». El border (1px) nunca fue el problema. Fix: .btnL.outline { outline: none; ... } + foco accesible propio vía .btnL.outline:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px }. Al crear cualquier clase que se llame igual que una utilidad de Tailwind (outline, ring, shadow, border, etc.), verificar el estilo computado (no solo el border) para descartar colisiones.

16.4 Revisión en vivo y refinamientos (Frank 2026-08-09) — SOLO diseño

Segunda pasada de diseño sobre /perks y la ficha (revisión en vivo con Frank). NO se tocó backend, RLS ni el flujo de canje. Componentes: perks/+page.svelte, perks/[id]/+page.svelte, src/lib/styles/perks.css, LapdiSiteHeader.svelte, LapdiSiteFooter.svelte.

17. Trazabilidad y compliance (Frank 2026-07-06) — REGLA DEL SISTEMA

Dos bitácoras append-only (nunca UPDATE ni DELETE; el histórico es la evidencia legal):

Regla obligatoria (nuevas acciones)

Toda acción sensible NUEVA que se agregue al sistema DEBE registrarse con logAction() en action_audit_log. Es requisito de compliance, no opcional. Cuenta como "sensible" cualquier acción que: - confirme o rechace un rol/invitación por correo (aceptar CEO, miembro, contacto), - transfiera, elimine, restaure o cierre una entidad o cuenta, - cambie credenciales o estado legal (consentimiento, versión aceptada), - ejecute un cambio irreversible o de titularidad.

Cada registro lleva: quién (actor_user_id + actor_email), qué (action_type), sobre qué (entity_type + entity_id), cuándo (created_at), por dónde (channel: web/email/api/cron), IP y user-agent. logAction es fire-and-forget: nunca debe tumbar el flujo de negocio.

Acciones cubiertas hoy: team_invitation_accepted/rejected, ceo_accepted, transfer_initiated/accepted/rejected/cancelled/expired, account_deletion_requested/confirmed, account_recovered, entity_deleted/restored, consent_accepted, consent_reaccepted_tacit. Si agregas una nueva, añade el action_type al CHECK de la tabla y a AuditAction en audit.ts.

Sesión (Frank 2026-07-06)


18. Filtros dependientes / facetas acumulativas (Frank + Roberto 2026-07-10) — REGLA DEL SISTEMA

Problema que evita esta regla: filtros independientes permiten combinaciones sin resultados (ej. país=Colombia + ciudad=Lima → 0 resultados), porque cada dropdown lista TODOS los valores posibles de su columna sin tener en cuenta los demás filtros ya activos.

Regla obligatoria: en cualquier panel con múltiples filtros simultáneos, las opciones de CADA filtro se calculan sobre los registros que pasan los DEMÁS filtros activos — nunca el propio. Es decir, un filtro nunca se filtra a sí mismo al calcular sus opciones, pero sí queda acotado por todos los otros filtros encendidos. El resultado: solo aparecen como opción los valores que, combinados con la selección actual, producen al menos un resultado.

Implementaciones de referencia

Filtro huérfano (valor seleccionado que deja de tener opciones)

Si el usuario tenía un valor elegido en un filtro y otro filtro cambia de forma que ese valor ya no aparece entre las opciones válidas (p. ej. cambia de país y la ciudad que tenía seleccionada no existe en el país nuevo), el filtro huérfano se ignora con gracia: el loader lo excluye de la query principal y de las facetas devueltas al cliente (no se aplica un filtro fantasma que dejaría 0 resultados), y el store del filtro en el cliente se resincroniza al valor efectivo (vacío) en el siguiente render. No hace falta que el usuario limpie el filtro a mano.

Excepción documentada: industria

El filtro de industria en Startups/ICG es no-op (LAP-259 hotfix: industry_id no existe como columna en startups/icgs, la industria vive en JSONB/array y se reimplementará con contains_any). Como no filtra resultados reales, su lista de opciones es el catálogo completo (catalogo_industria) sin acotar — no participa del cálculo de facetas hasta que el filtro de industria sea funcional.

Alcance actual

Aplicado a país ↔ ciudad en ambos paneles de consulta de admin (los dos filtros primarios geográficos). El filtro de tipo de empresa/ICG en el sidebar (selectedTypes) sigue siendo client-side sobre la página cargada (12 registros) — no está cableado a la URL/servidor todavía, así que no participa de las facetas dependientes; es una limitación preexistente, no introducida por esta regla.

La presentación de estos filtros (la barra horizontal con búsqueda + botón «Filtros» + «Descargar reporte») está estandarizada en el componente DashFilterBar — ver §19. Esa barra es solo de presentación: la lógica de facetas dependientes descrita arriba sigue viviendo en cada panel.

19. Barra de filtros estándar del dashboard (DashFilterBar) — REGLA DEL SISTEMA (Frank 2026-07-10 → 2026-07-12)

Fuente única: src/lib/components/dashboard/DashFilterBar.svelte. Todo panel del dashboard con filtros usa esta barra; PROHIBIDO volver a maquetar un toolbar/panel de filtros propio por página.

Anatomía (dos zonas)

  1. Fila fija (siempre visible): una sola línea horizontal con, en este orden: - Búsqueda por texto (input con lupa a la izquierda y «✕» circular azul para limpiar cuando hay texto). Ocupa el espacio libre. Solo aparece si el panel provee una dim search. - Botón «Filtros» con el ícono de embudo (filter). Muestra un badge azul con el número de filtros activos. Al pulsarlo despliega/colapsa el panel de filtros. - Botón «Descargar reporte» con el ícono de descarga (download). Aparece SOLO si el panel pasa la prop onDownload. Nombre único obligatorio: «Descargar reporte» — quedan PROHIBIDOS los nombres antiguos «Descargar», «Descargar CSV», «Exportar CSV». Es el único botón de descarga del panel: no debe existir un botón de descarga separado fuera de la barra.
  2. Panel colapsable (oculto por defecto): contenedor bordeado con las dimensiones de filtro. Arranca colapsado en todos los tamaños; se abre al pulsar «Filtros». Incluye un botón «Limpiar» (ícono eraser) que aparece solo cuando hay filtros activos.

Contrato (props y eventos)

Presentación vs. lógica (regla)

La barra es puramente de presentación: NO calcula el acotamiento de opciones. Cada panel mantiene su propia lógica de facetas dependientes (§18) — matchExcept(registro, except, filtros) + optBase (base de opciones = búsqueda aplicada, sin las dims) — y le pasa a la barra las opciones ya acotadas por dimensión. Los filtros son acumulativos/dependientes: cada dropdown lista solo valores que, combinados con los demás filtros activos, producen al menos un resultado; el valor ya seleccionado se conserva siempre. La reactividad usa params explícitos: matchExcept(s, except, _f) recibe base y estado de filtros como argumentos para que Svelte recalcule las opciones al cambiar cualquier fil*.

Íconos estándar (autosuficientes)

Los íconos propios de la barra se toman de EVENT_ICON (src/lib/data/eventoIcons) e se insertan con solo viewBox (sin stroke/fill): la barra define el trazo de SUS íconos vía CSS acotado, así se ve igual con o sin ancestro .eventos-surface. Ícono de filtros = embudo (filter); ícono de descarga = flecha a bandeja (download); búsqueda = search; calendario = calendar; limpiar = eraser.

Estilo

Compacto: controles a 34–38px de alto, selects/multis con ancho uniforme (flex-basis fijo ~184px), radios de 10–12px, paleta de marca con fallbacks (--lpdi-navy, --lpdi-brandeis, --slate-*). Responsive: bajo 768px la fila fija se apila y los controles ocupan el ancho completo.

Implementaciones de referencia


20. Tabla ancha del dashboard — REGLA CANÓNICA DEL SISTEMA (Frank 2026-07-12)

Por qué existe esta regla: las tablas con muchas columnas (eventos, supereventos, auditoría, consultas) se venían ajustando caso por caso (un max-width distinto por página, una columna de acciones alineada distinto, la pista de scroll a veces sí y a veces no). Frank preguntó por qué no hay una sola regla. Esta es esa regla: toda tabla ancha del dashboard cumple los MISMOS valores fijos. PROHIBIDO ajustar una tabla ancha con valores propios «solo para esta página»; si un caso no encaja, se cambia la regla aquí y se propaga.

Una tabla es «ancha» cuando sus columnas no caben sin scroll horizontal en un viewport de escritorio típico (≈1440px). Aplica a paneles de admin y del usuario por igual.

20.1 Valores fijos obligatorios

  1. Contenedor de página: max-width: min(1680px, 96vw); margin: 0 auto; (mismo valor que §15.5 para admin; el contenedor del usuario .me-wrap usa idéntico min(1680px, 96vw) porque su tabla espeja la de admin). NUNCA un max-width fijo distinto (ej. 1400px) «para que se vea menos limitada»: el estándar ya es el ancho.
  2. Wrap de la tabla (overflow-x: auto): borde 1px solid var(--border) y radio inferior 0 0 12px 12px (el radio superior lo aporta la pista/barra de scroll superior; ver punto 5). Fondo var(--bg-surface).
  3. Encabezado gris (thead th): background: var(--bg-elevated); + text-align:left, text-transform:uppercase, font-size:~11.5px, color:var(--text-muted), border-bottom:1px solid var(--border), white-space:nowrap. El mismo gris se aplica a la celda de encabezado de Acciones (thead .*-th-actions), con un z-index mayor por ser sticky en dos ejes.
  4. Columna «Acciones» FIJA (sticky a la derecha): position:sticky; right:0; con sombra de separación box-shadow: -6px 0 8px -6px rgba(0,0,0,.12); y fondo propio (surface en el body, elevated en el hover y el thead) para que el contenido se «esconda» debajo al desplazar. Rótulo y botones alineados a la derecha y COINCIDENTES: el th lleva text-align:right; el contenedor de botones de la celda debe quedar también a la derecha. Si los botones van en un contenedor flex, ese contenedor lleva justify-content: flex-end (un display:flex sin esto los deja a la izquierda y el rótulo queda «corrido»). Si los botones son hijos inline directos de la celda, text-align:right ya alinea los botones sin contenedor extra. Alineación + ancho (Frank 2026-07-12): el rótulo «Acciones» y los botones van alineados a la IZQUIERDA (text-align: left), pegados a la última columna de datos, y la columna usa width: 1% (shrink-to-content) para ajustarse al ancho de sus botones. Sin width:1%, cuando las columnas de datos son angostas la última columna se estira a llenar el contenedor y deja un hueco vacío grande a la derecha de los botones. La alineación a la derecha en una columna ancha también dejaba una banda vacía entre los datos y los botones. Aplica a TODA tabla con acciones (mios, super-eventos, consulta startups/icg, beneficios/admin, aprobacion). Gotcha de especificidad (obligatorio): la alineación del th de Acciones se fija con un selector ESPECÍFICO .tabla thead th.*-th-actions { text-align: left } (especificidad 0,2,2). Un simple .*-th-actions { text-align: left } (0,1,0) queda igual que la genérica; usa el selector específico para que el rótulo y los botones queden consistentes. Además la columna se dimensiona al contenido (no un ancho fijo mayor) para no reintroducir el hueco. Aplica a TODA tabla con columna de acciones, no solo a las anchas sticky: las tablas .adm-table (consulta/startups, consulta/icg, beneficios/admin) usan .adm-table thead th.adm-th-actions { text-align: right } por la misma razón. Al crear una tabla nueva con acciones, replica el selector específico.
  5. Pista de scroll horizontal OBLIGATORIA arriba de la tabla (texto .scroll-hint, ícono de flechas ‹›): «Desliza horizontalmente para ver todos los campos… Las Acciones quedan fijas mientras te desplazas. Puedes ajustar el ancho de cada columna arrastrando su borde derecho.» Debajo de la pista va la barra de scroll superior sincronizada (estándar admin): un div con overflow-x:auto y un spacer del ancho de la tabla (tableEl.scrollWidth), sincronizado con el wrap inferior vía syncFromTop/syncFromBottom (guardas bind:this a topScrollEl/bottomScrollEl). Radio superior 12px 12px 0 0, borde sin border-bottom.
  6. Columnas redimensionables vía use:colResize={{ storageKey, deps, skipClass: '*-th-actions' }} de $lib/actions/colResize; el ancho de las columnas de datos se persiste en localStorage. afterUpdate llama applyCellTitles(tableEl) para poner title a las celdas truncadas.

REGLA DE DIMENSIONADO DE LA COLUMNA «ACCIONES» (Frank 77, 2026-07-23) — obligatoria para TODA tabla con columna de acciones, no solo las anchas: la columna sticky de Acciones se pasa como skipClass y se dimensiona SIEMPRE por su contenido real (ancho de los botones), NUNCA desde un ancho persistido ni un valor fijo. El juego de botones cambia por estado y permiso de cada fila, así que un ancho guardado obsoleto cortaba los botones sin dejar handle para ensanchar (por eso le pasaba a unos usuarios y a otros no). El helper colResize ya lo implementa así; NO reintroducir un ancho fijo/persistido para esa columna. Requisitos verificados que debe cumplir cualquier reimplementación: - Medir bajo table-layout:auto (limpiando el ancho de la sticky) para que el scrollWidth no salga constreñido cuando la tabla ya venía en fixed. - Respaldar la medición con la suma real del offsetWidth de los botones + paddings (por si la celda ya venía clipeada). - Re-medir tras cambios tardíos de layout, no solo al montar: en el siguiente frame (requestAnimationFrame), cuando las fuentes cargan (document.fonts.ready — con la fuente fallback los botones miden menos y la columna quedaba corta hasta un F5), y ante alta/baja de filas o botones (MutationObserver sobre el tbody, solo childList). Todas las re-mediciones son idempotentes. Consumidores actuales del mismo colResize: eventos/mios, beneficios/admin, eventos/admin/aprobacion, eventos/admin/super-eventos. Un cambio en el helper cubre a las cuatro.

20.2 Implementaciones de referencia (copiar de aquí)

20.3 Nota sobre reutilización

El «estándar» es documental + valores idénticos entre implementaciones, no un componente compartido: mios y super-eventos manejan su propio bottomScrollEl/syncFromBottom y su colResize con storageKey propio. PROHIBIDO extraer un componente único si eso rompe el scroll sincronizado o el redimensionado por columna; lo que se comparte son los valores de esta sección, verificados idénticos entre páginas.

20.4 Capa visual estándar (Frank 2026-07-28) — colores, columna principal, alto de fila, barras y acciones

Complemento de §20: los criterios visuales que TODA tabla (ancha o no) debe cumplir para verse idéntica. Frank preguntó por qué unas tablas tenían colores/estilos distintos; esta subsección los unifica. PROHIBIDO desviarse por página.

  1. Colores de texto y títulos. Encabezados de columna (thead th) en azul marca #003e88 (--text-primary) sobre fondo gris claro var(--bg-elevated) (#f1f5f9). Texto de celdas de datos en gris var(--text-secondary) (#64748b). PROHIBIDO negros puros (#000) o colores sueltos por columna. (Refina el color del thead th de §20.1.3, que antes usaba --text-muted: el encabezado va ahora en #003e88, manteniendo el resto — uppercase, ~11.5px, nowrap, bg-elevated.)
  2. Columna principal (primera). Es el identificador de la fila (Nombre). Su texto va en azul marca #003e88, font-weight:700, y la columna queda fija (sticky a la izquierda) al hacer scroll horizontal, con la misma sombra de separación que la de Acciones pero del lado izquierdo (box-shadow: 6px 0 8px -6px rgba(0,0,0,.12)) y fondo propio (surface en body, elevated en thead/hover). Resultado: la primera columna (Nombre, sticky-left) y la última (Acciones, sticky-right) quedan ancladas; las columnas de datos se desplazan entre ambas.
  3. Alto de fila fijo. Las filas NO cambian de alto según el contenido: altura pareja en toda la tabla. El texto largo se corta con elipsis (overflow:hidden; text-overflow:ellipsis; white-space:nowrap) y muestra el valor completo en title (tooltip) vía applyCellTitles. Se logra con nowrap+overflow:hidden+ellipsis en las celdas y los anchos que persiste colResize (patrón de eventos/mios), SIN table-layout:fixed. Las celdas con chips (temáticas, etc.) llevan su contenedor en flex-wrap:nowrap para que no crezcan de alto. Ninguna fila «salta» de alto respecto a las demás.
  4. Dos barras de scroll horizontal, en azul (Frank corrigió: NO una sola). Se mantienen las dos barras sincronizadas de §20.1.5 (superior sobre la tabla + inferior en el wrap). Ambas se estilizan con el azul del DS: ::-webkit-scrollbar-thumb { background: var(--accent); border-radius:6px } y ::-webkit-scrollbar { height:10px } sobre .top-scroll y el wrap, con scrollbar-color: var(--accent) transparent para Firefox. No eliminar la barra superior: Frank la quiere presente arriba y abajo.
  5. Columna «Acciones» en una sola línea, iconos soft CON borde. Los botones/íconos de acción van en una sola línea (white-space:nowrap, sin flex-wrap). Estilo soft: ícono a color sobre fondo tintado suave (~12%) del mismo color, CON borde de 1px del mismo color (Frank corrigió: con borde, como el catálogo de íconos, no sin borde). Colores por tipo de acción según el catálogo (playgrounds.digitalhubassist.ai/lpdi-iconos-ds.html): Editar=azul, Aprobar/positivo=verde, Precaución=ámbar, Cierre/Eliminar=rojo, Utilitario/Evaluar=gris, Destacar=dorado.
  6. Pista de ayuda solo cuando desborda (Frank corrigió §20.1.5). El texto .scroll-hint («Desliza horizontalmente…») se muestra únicamente cuando la tabla realmente se desborda (tableEl.scrollWidth > wrapEl.clientWidth), no siempre. Mismo texto en todas las tablas. Se re-evalúa en afterUpdate y ante resize.
  7. GOTCHA obligatorio de la columna fija (Frank item 1, 2026-07-29) — REGLA DEL SISTEMA. La regla que da position: relative a los thead th (necesaria para el agarre .col-resize) tiene MÁS especificidad que la que fija la columna principal (position: sticky), así que le gana y el encabezado de la primera columna deja de ser fijo y su título se pierde al desplazar (el cuerpo sí queda fijo porque no hay regla td compitiendo). SIEMPRE hay que excluir la columna sticky de la regla relative: thead th:not(.<sticky-actions>):not(.<sticky-nombre>) { position: relative }. Aplica a TODA tabla con columna fija + redimensionado. Verificar SIEMPRE con un render desplazado a la derecha, no solo al inicio.
  8. Columna ordenada marcada (Frank #21, 2026-07-29/30). La columna elegida para ordenar se marca claramente: el activo muestra un triángulo SÓLIDO ▲/▼ (relleno, fill:currentColor) en var(--accent) + fondo tinte rgba(0,110,255,.08) en el th; los inactivos muestran un chevron tenue (afordancia de «ordenable»). Un chevron de línea fina en el activo NO alcanza (se confunde con los inactivos): usar el triángulo relleno, como en Directorio y Aprobación. Clase .sorted-active en el th según el estado de orden.

Orden de alineación (Frank 2026-07-28): Directorio de organizadores → Beneficios/admin → Aprobación de eventos → consultas de startups/ICG. Cada tabla se verifica con captura (render con scroll horizontal) antes de marcar hecha.

21. Registro de entidades y pantallas de ciclo de vida de la cuenta (Frank 2026-07-13) — REGLA DEL SISTEMA

21.1 Camino de registro (actualizado Frank #37a, 2026-07-29)

Registrar una entidad nueva abre el formulario simplificado (FSS dashboard/startups/nuevo, FIS dashboard/icg/nuevo-simplificado). Aplica a los enlaces del sidebar, a los modales de «crear nueva» (onCreateNew) y a la card «Registrar mi iniciativa» del ecosistema. El formulario detallado (FSD dashboard/startups/nuevo-detallado, FID dashboard/icg/nuevo) se alcanza al completar/actualizar el perfil desde PU, al editar una entidad existente (onEditExisting) o al elegir la entidad activa (no borrador) en el sidebar. Todas las rutas siguen vivas (formularios a medio llenar y enlaces guardados no deben romperse con un 404).

21.2 Los paneles NO se bloquean

El usuario navega libremente por todos los paneles desde el primer momento y todo se autoguarda como borrador. Prohibido volver a bloquear paneles a la espera de que la entidad se registre.

21.3 El botón «Registrar» vive fuera del panel 1

Se llama «Registrar» (no «Registrar y continuar»), aparece en la barra de acciones de todos los paneles y valida los mínimos desde donde sea que esté el usuario. Si falta algo, lo dice y lleva al panel del campo que falta. Cuando la entidad ya está registrada, el botón se reemplaza por su estado. El cliente solo se da por registrado cuando el servidor lo confirma; nunca por su cuenta.

21.4 El asterisco marca solo los mínimos reales

Formulario Campos con *
Startup detallado nombre comercial, país de origen
ICG detallado rol en el ecosistema, nombre del ICG (o nombres/apellidos del inversionista individual), industria en la que opera, país de operación, correo corporativo

El país de origen del ICG NO es obligatorio. Los datos de contacto del ICG tampoco. Los mínimos los fija la base (columnas NOT NULL sin default), no el gusto: startups exige company_name, country, contact_email; icgs exige organization_name, contact_email. El correo se toma de la sesión, no se pregunta.

21.5 Datos personales de terceros: se leen, no se diligencian

En el panel de contacto del ICG, nombres, apellidos, LinkedIn y teléfono están siempre en solo lectura. Si el correo pertenece a un usuario registrado, se traen de su perfil; si no, quedan vacíos y se llenarán cuando esa persona acepte y complete su perfil. El cargo sí es editable: depende de la relación de esa persona con esa entidad, no de su perfil. Razón (Frank): por términos y condiciones no se capturan los datos personales de otra persona desde el formulario de una entidad.

21.6 Pantallas de cuenta eliminada

/cuenta-eliminada usa los tokens del sistema (§2). Prohibido el fondo degradado verde oscuro y el botón verde que tenía: el acento es azul y no existe ningún tema verde (§1.2). - Tras eliminar la cuenta, el usuario no cae en /login en seco: ve una despedida (?justDeleted=1, funciona sin sesión porque el borrado cierra la sesión en el servidor) con una cuenta regresiva que lo lleva a eco.lpdi.co. - Quien vuelve dentro de los 30 días ve su botón de recuperar cuenta, sin cuenta regresiva que lo expulse mientras intenta recuperarse. - «Cerrar sesión y salir» cierra la sesión de verdad (acción ?/logout) y sale a eco.lpdi.co. Prohibido dejarlo como un enlace a /login: la sesión quedaba viva y el usuario en el limbo.


22. Beneficios públicos, alianzas y accesos (Frank 2026-07-22) — REGLA DEL SISTEMA

22.1 Catálogo de beneficios PÚBLICO

/perks (listado y ficha de aliado) es de acceso público, sin sesión. El filtro de estado es explícito en el query (status='publicado' + deleted_at IS NULL), no se delega a la RLS. El canje sí exige iniciar sesión. La card «Beneficios del ecosistema» de la home (/ecosistema, que sirve /) es visible para todos, ubicada tras «Registrar mi iniciativa»; sin sesión muestra solo el botón «Ver catálogo» (nunca «Publica tus beneficios»).

22.2 «¿Para quién es el beneficio?» — beneficiario multi-select (SSOT src/lib/data/perk-audiences.ts)

Cuatro opciones fijas: Startups · PyMEs · Corporativos · Usuario final (selección MÚLTIPLE, obligatoria). En los formularios (FP y FP externo) se muestran como lista de casillas desplegada, SIN íconos. Los íconos (AudienceIcon.svelte) van solo en /perks (filtro y tarjetas) y en la ficha del aliado. Columna DB perks.audiences text[]; la columna legacy perks.audience se conserva derivada (deriveLegacyAudience) para lectores viejos.

22.3 «Países donde aplica el beneficio» (SSOT src/lib/data/perk-countries.ts)

Pregunta obligatoria, justo después de beneficiario, con el RegionCountryPicker (multi) más una opción explícita «Todos los países». Semántica: perks.benefit_countries text[] vacío = aplica a todos. Donde se muestra (ficha, tarjeta de /perks, admin) la ausencia de países se rotula exactamente «Todos los países».

22.4 Filtros y columnas de beneficio

22.5 Permiso «Publicar beneficios» y flujo «Quiero ser aliado»

Publicar beneficios ya no se gatea por lista de correos (canAccessPerks, DEPRECADO): se gatea por el permiso «Publicar beneficios» (rol publicador del módulo beneficios en module_permissions, RPC can_publish_perks) más tener una entidad activa (formalizada). El superadmin lo otorga en /dashboard/admin/permisos. - En el sidebar, la sección Beneficios es visible para todos: «Ver catálogo» y «Publicar beneficio» siempre; «Mis beneficios» y «Mis referidos» solo con el permiso. - Al pulsar «Publicar beneficio» sin ser elegible, en vez de rebotar se muestra: «Para publicar beneficios en el Ecosistema LPDI necesitas primero formalizar una alianza con nosotros. Envíanos una solicitud de alianza usando el botón a continuación:» con el CTA «Quiero ser aliado». - «Quiero ser aliado» abre un pop-up (SolicitudAlianzaModal.svelte, patrón de IdeasFeedbackModal): nombre/correo prellenados, selector de entidades activas (o «Aún no tengo una entidad registrada en el sistema»), texto obligatorio, y dos botones excluyentes «Enviar mensaje» / «Enviar mensaje y agendar una reunión». POST /api/alianzas envía el correo a PARTNERS_EMAIL con copia al usuario y audita (alianza_solicitada); el segundo botón además abre la URL de agenda tras el envío.

22.6 Rutas de login y redirect post-registro

22.7 Columna «Acciones» de las tablas anchas (refina §20)

La columna sticky de acciones (no redimensionable, skipClass) se dimensiona siempre por el contenido real de sus botones (colResize.ts mide el scrollWidth de la celda más ancha) y no persiste su ancho. Prohibido darle un ancho fijo desde localStorage: el juego de botones cambia por estado/permiso y un ancho obsoleto cortaba los botones sin dejar handle para ensancharla.

23. Códigos únicos: nada en borrador recibe código — REGLA TRANSVERSAL DEL SISTEMA (Frank 2026-07-23)

Regla: los identificadores/códigos únicos del sistema (código de entidad, evento_codigo y codigo_unico de eventos, y los códigos de convocatorias, noticias y cualquier módulo futuro) se asignan únicamente al formalizar / enviar el registro, nunca al crear o autoguardar un borrador. Un borrador abandonado no debe quemar números ni dejar identificadores huérfanos.

Motivo: los borradores por autosave se crean masivamente y muchos se abandonan; si el código se asigna al insertar la fila del borrador, la numeración se llena de huecos y de identificadores sin dueño real. El código es un identificador estable de un registro ya formalizado, no de un borrador en construcción.

Estado por identificador (verificado 2026-07-23): - Entidades (entity_code, formato S-TTT-PPP-AA-NNNNN / I-TTT-PPP-AA-NNNNN): nace conforme — se asigna solo en formalizeEntity(). - Eventos evento_codigo (formato M-PPP-TTT-AA-NNNNN): conforme — se genera solo en el submit final; el autosave lo deja NULL. - Eventos codigo_unico (era SERIAL): conforme desde la migración 134 (tarea 67) — se le quitó el DEFAULT nextval y el NOT NULL (la secuencia eventos_codigo_unico_seq se conserva), se anuló el código de los borradores (status='DRAFT') y se asigna al enviar vía la RPC atómica e idempotente assign_evento_codigo_unico(uuid) (solo si el código está NULL). Los eventos ya publicados conservan su número y sus enlaces /eventos/{codigo} (cero renumeración). En "Mis eventos" un borrador muestra "—" en la columna Código. super_eventos queda fuera (admin-only, sin flujo de borrador).

Al agregar un identificador nuevo a cualquier módulo, respeta esta regla desde el diseño: la asignación va en el punto de formalización/envío, no en el insert del borrador.