higinio.sosa Nocturna Eléctrica · v2.0

Nocturna Eléctrica

Sistema de diseño dark-first, enérgico y de alto contraste. El fondo casi negro es el escenario; el celeste eléctrico es el reflector.

#52baed · celeste eléctrico #353d7c · índigo profundo #010203 · negro nocturno

Principios

  1. Contraste con propósito — el acento solo aparece donde hay acción o dato clave.
  2. Profundidad por luz — la elevación aclara superficies, no oscurece bordes.
  3. Energía contenida — paleta vibrante, espaciado disciplinado: nunca ruidosa.
  4. Datos protagonistas — cifras y gráficas brillan; el chrome se mantiene discreto.
  5. Movimiento con propósito — la animación explica cambios de estado, nunca decora sola.
Regla de oro: los componentes consumen solo tokens semánticos (--surface-1, --text-primary…), nunca hex directos. El cambio de tema es un cambio de atributo, no un refactor. Pruébalo con el botón de tema de la esquina superior.

Voz y contenido

Idioma y tono

Español, técnico pero cercano. Tuteo implícito. Frases cortas y declarativas, con energía contenida.

Casing

Sentence case en headings y botones: «Crear proyecto», no «Crear Proyecto». Overlines en uppercase con tracking +0.08em.

Botones

Verbo + objeto, corto: «Guardar cambios», «Ver informe».

Errores

Concretos y accionables: dicen qué pasó y qué hacer.

Datos

Siempre en JetBrains Mono o tabular-nums; grandes en dashboards, con count-up de 350ms al montar.

Emoji

No se usan. El estado se comunica con color + icono + texto.

Colores

Haz clic en cualquier swatch para copiar su variable CSS.

Acento — Celeste Eléctrico

Secundario — Índigo Profundo

El índigo #353d7c sobre negro da un contraste de 2.09:1 — nunca es texto sobre fondos nocturnos, solo fondos de sección, sidebars y estados seleccionados.

Neutrales — Escala Nocturna

Semánticos

Regla 60-30-10

60% fondos nocturnos · 30% texto y estructura (índigo como fondo de marca) · 10% acento eléctrico y semánticos. El celeste es el único color que grita: un CTA primario por pantalla. Texto masivo en night-100, nunca blanco puro.

Tipografía

Space Grotesk · displayEnergía contenida
Manrope · body/UIEl chrome discreto deja brillar los datos.
JetBrains Mono · datos1 024 768,50
Space Grotesk se reserva para h1–h3, cifras hero y navegación. h4 en adelante usa Manrope. En dark, el texto claro «engorda»: preferir peso 400 donde en light usarías 500.

Escala (ratio 1.25, base 16px)

Espaciado

Escala base 4px. Aliases de uso: --card-padding: 24px · --section-gap: 64px · --form-gap: 20px · --touch-target: 44px.

Bordes, radios y focus

sm · 8
md · 12
lg · 16
xl · 24
full

Borde por defecto 1px night-500; énfasis night-600. Focus ring global de teclado: 2px accent-400 con offset 2px — pruébalo navegando con Tab.

Elevación

Sobre negro las sombras solas no bastan: la elevación combina superficie más clara + sombra + borde sutil.

Nivel 0
night-950
Nivel 1
night-900 + shadow-1
Nivel 2
night-800 + shadow-2
Nivel 3
night-800 + borde night-600 + shadow-3

Glow de acento

glow-accent
máximo uno por pantalla

Iconografía

Set sustituto: Lucide — trazo 2px, esquinas redondeadas, fill: none, tamaño default 20px, color currentColor. Tamaños: 20px en botones · 18px en inputs · 16px en chevrons. Sin emoji, sin unicode-como-icono.

home
chart
bell
search
plus
bolt

Marca / wordmark

No hay logo dibujado: el wordmark es tipográfico — Space Grotesk 700, tracking -0.02em, con el punto separador en accent-400.

higinio.sosa higinio.sosa
Biblioteca

Componentes

Los 35 componentes del sistema, renderizados en vivo con las clases del framework nocturna-electrica.css. Cada bloque incluye su código de uso.

Botones

Alturas 36 / 44 / 52px. El primario lleva texto oscuro (contraste 9.50:1) — nunca blanco. Un solo primario por pantalla.

Ver código
<button class="btn btn--primary">Guardar cambios</button>
<button class="btn btn--secondary">Ver informe</button>
<button class="btn btn--outline">Exportar</button>
<button class="btn btn--ghost">Cancelar</button>
<button class="btn btn--danger">Eliminar cuenta</button>

<!-- Tamaños: .btn--sm (36px) · default (44px) · .btn--lg (52px) -->
<!-- Loading: fijar min-width y sustituir el texto por .spinner -->

IconButton

Botón cuadrado solo-icono. Siempre con aria-label.

Ver código
<button class="icon-btn icon-btn--outline" aria-label="Buscar">
  <svg>…</svg>
</button>
<!-- Variantes: --outline · --ghost · --accent | Tamaños: --sm (36) · default (44) · --lg (52) -->

Spinner

Trazo 2px, rotación 0.8s. Si la carga dura menos de 300ms, no mostrar spinner.

Ver código
<span class="spinner"></span>
<!-- Hereda currentColor; default accent-400 -->

Input

Fondo --surface-2, label siempre visible (nunca placeholder-como-label).

Nunca compartiremos tu correo.
El correo no es válido — revisa el dominio.
El asterisco es decorativo; lo obligatorio lo marca required.
Ver código
<div class="field">
  <label class="field__label" for="email">Correo</label>
  <input class="input" id="email" type="email" placeholder="tu@correo.com">
  <span class="field__help">Texto de ayuda.</span>
</div>
<!-- Obligatorio: .field--required en el .field pinta el asterisco (decorativo); -->
<!-- el control lleva required + aria-required="true" -->
<div class="field field--required">
  <label class="field__label" for="name">Nombre</label>
  <input class="input" id="name" type="text" required aria-required="true">
</div>
<!-- Estados: .input--error (+ .field__error) · .input--success · disabled -->
<!-- Icono leading: envolver en .input-wrap con un svg antes del input -->

Textarea

Fondo --surface-2, label siempre visible (nunca placeholder-como-label).

0 / 280
Ver código
<div class="field">
  <label class="field__label" for="desc">Descripción</label>
  <textarea class="textarea" id="desc" placeholder="Describe el proyecto…"></textarea>
  <span class="field__count">0 / 280</span>
</div>

Select

Ver código
<div class="select-wrap">
  <select class="select"><option>Opción</option></select>
</div>

SegmentedControl

2–4 opciones mutuamente excluyentes. Contenedor --surface-2, opción activa en --surface-3 con texto --accent-300 y peso 600. Para más de 4 opciones, usar Select.

Ver código
<div class="segmented" role="radiogroup" aria-label="Vista">
  <input type="radio" name="vista" id="lista" checked><label for="lista">Lista</label>
  <input type="radio" name="vista" id="tablero"><label for="tablero">Tablero</label>
</div>
<!-- Compacto: añadir segmented--sm -->

Slider

Track 6 px, porción rellena en --accent-400 sobre --surface-3; thumb --night-50 con borde de acento que escala 1.15 al presionar.

Límite de gasto $ 350
Ver código
<div class="slider-row">
  <span class="slider-label">Límite de gasto</span>
  <span class="slider-value">$ 350</span>
</div>
<input class="slider" type="range" min="0" max="1000" value="350">
<!-- JS: --slider-fill = ((v-min)/(max-min))*100 + '%' -->

Checkbox y Radio

Ver código
<label class="check-row"><input type="checkbox" class="checkbox"> Etiqueta</label>
<label class="check-row"><input type="radio" name="g" class="radio"> Opción</label>
<!-- Indeterminate = tercer estado (guion horizontal). Solo por JS: -->
<!-- document.querySelector('.checkbox').indeterminate = true; -->

Switch

Ver código
<label class="check-row"><input type="checkbox" class="switch"> Activar</label>

Badge y Tag

Activo Pendiente Error Beta diseño frontend
Ver código
<span class="badge badge--success">Activo</span>
<span class="tag">diseño <button class="tag__x" aria-label="Quitar">×</button></span>

Avatar

Iniciales en Space Grotesk sobre índigo; punto de estado opcional.

hs hs hs hs hs
Ver código
<span class="avatar">HS<span class="avatar__dot avatar__dot--online"></span></span>
<!-- Tamaños: --xs 24 · --sm 32 · default 40 · --lg 48 · --xl 64 -->
<!-- Con imagen: <span class="avatar"><img src="…" alt="Nombre"></span> -->

AvatarGroup

Avatares solapados 8px con borde 2px del color del fondo; el chip .avatar-group__more resume los ocultos.

hs ml ar jt +3
Ver código
<div class="avatar-group" role="group" aria-label="Equipo del proyecto">
  <span class="avatar">HS</span>
  <span class="avatar">ML</span>
  <span class="avatar">AR</span>
  <span class="avatar avatar-group__more">+3</span>
</div>
<!-- Solapamiento -8px + borde del color del fondo (--surface-1 / --bg). -->

Divider

Regla hairline de 1px sobre --border; variante con label centrado y variante vertical para separar acciones en línea.


FiltrosOrdenarExportar
Ver código
<hr class="divider">
<div class="divider--label" role="separator">o continúa con</div>
<span class="divider--vertical" role="separator"></span>

Accordion

Sobre fondo casi negro la elevación combina superficie más clara + sombra + borde sutil. Las sombras solas no bastan.
Solo en interactivos clave y máximo uno por pantalla: es la firma de la marca, no una decoración repetible.
Negro nocturno sobre celeste eléctrico da 9.50:1 de contraste; blanco sobre celeste no pasa AA.
Ver código
<div class="accordion">
  <div class="accordion__item">
    <button class="accordion__header" aria-expanded="false">
      Pregunta <svg>chevron</svg>
    </button>
    <div class="accordion__panel"><div class="accordion__inner">
      <div class="accordion__content">Respuesta…</div>
    </div></div>
  </div>
</div>
<!-- JS: alternar aria-expanded en el header -->

Tooltip

Delay de 400ms; máximo una línea. Pasa el cursor:

Exportar como CSV Ajustes
Ver código
<span class="has-tooltip">
  <button class="btn btn--outline">Pasa el cursor</button>
  <span class="tooltip" role="tooltip">Exportar como CSV</span>
</span>

Alert

Banner inline y persistente (a diferencia del Toast, que es transitorio). Icono + texto + color — el estado nunca depende solo del color.

Nueva versión disponible

La versión 2.4 incluye exportación a PDF. Actualiza cuando quieras.

Pago confirmado

El recibo se envió a tu correo.

Tu plan expira en 5 días

Renueva antes del 12 de julio para no perder acceso.

No se pudo guardar el formulario

Revisa los campos marcados e inténtalo de nuevo.

Ver código
<div class="alert alert--warning" role="status">
  <svg>…icono…</svg>
  <div>
    <p class="alert__title">Tu plan expira en 5 días</p>
    <p class="alert__body">Renueva antes del 12 de julio.</p>
  </div>
  <button class="alert__close" aria-label="Cerrar aviso">…x…</button>
</div>
<!-- Variantes: --info · --success · --warning · --danger -->

Toast

Top-right, auto-dismiss a los 5s (pausable con hover), máximo 3 apilados.

Ver código
<div class="toast-stack" role="status" aria-live="polite">
  <div class="toast toast--success">
    <svg>…</svg>
    <div>
      <div class="toast__title">Cambios guardados</div>
      <div class="toast__body">El proyecto se actualizó correctamente.</div>
    </div>
  </div>
</div>

Progress y ProgressRing

Barra para procesos lineales (determinada/indeterminada); anillo circular para progreso compacto en cards y dashboards. Comparten --accent-400 y --data-color.

Subiendo archivos…64 %
Indeterminado
64%
Sincronización
Ver código
<div class="progress__head">
  <span class="progress__label">Subiendo…</span>
  <span class="progress__value">64 %</span>
</div>
<div class="progress" role="progressbar" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100">
  <div class="progress__bar" style="width:64%"></div>
</div>
<!-- Variantes: .progress--indeterminate · .progress--no-glow -->

<div class="progress-ring" role="progressbar" aria-valuenow="64" aria-valuemin="0" aria-valuemax="100">
  <svg width="56" height="56" viewBox="0 0 56 56">
    <circle class="progress-ring__track" cx="28" cy="28" r="24" stroke-width="5"/>
    <circle class="progress-ring__bar" cx="28" cy="28" r="24" stroke-width="5"/>
  </svg>
  <span class="progress-ring__value">64%</span>
</div>
<!-- JS: stroke-dasharray = 2πr; stroke-dashoffset = c * (1 - value/100) -->

Modal

Overlay con blur 4px; se cierra con Esc o clic fuera (excepto flujos destructivos con data-modal-persistent). Al abrir, el foco entra al panel y queda atrapado (Tab/Shift+Tab ciclan dentro); al cerrar, el foco vuelve al botón que lo abrió.

Ver código
<div class="modal-overlay">
  <!-- aria-modal, aria-labelledby (título) y aria-describedby (descripción, si la hay);
       tabindex="-1" permite mover el foco al panel al abrir -->
  <div class="modal" role="dialog" aria-modal="true"
       aria-labelledby="mTitle" aria-describedby="mDesc" tabindex="-1">
    <div class="modal__header">
      <h4 class="modal__title" id="mTitle">Título</h4>
      <button class="icon-btn icon-btn--ghost" data-close-modal aria-label="Cerrar">×</button>
    </div>
    <p id="mDesc">Contenido…</p>
    <div class="modal__footer">
      <button class="btn btn--outline" data-close-modal>Cancelar</button>
      <button class="btn btn--primary" data-close-modal>Confirmar</button>
    </div>
  </div>
</div>
<!-- JS: al abrir guarda document.activeElement y enfoca el panel; Tab cicla dentro
     (focus-trap); Esc / clic en overlay / data-close-modal cierran y devuelven el foco.
     Destructivo: añade data-modal-persistent al .modal-overlay. -->

Menu

Menú (dropdown)
Ver código
<div class="menu">
  <button class="menu__item">Duplicar</button>
  <button class="menu__item is-selected">Seleccionado</button>
</div>

Tabla

Zebra (.table--zebra) + encabezado fijo (.table--sticky, haz scroll dentro del recuadro) + columna ordenable (clic en «Proyecto» o «Ingresos») + columna de selección. La fila marcada usa .row--selected; el checkbox maestro está en estado indeterminate.

Estado Cambio
API de pagos Beta $31 990 +2,4 %
App móvil Pendiente $12 040 −1,1 %
Panel analítico Activo $48 210 +8,2 %
Portal de socios Activo $27 500 +5,0 %
Reportes Beta $9 800 −0,4 %
Ver código
<table class="table table--zebra table--sticky">
  <thead><tr>
    <th scope="col" class="col-select">
      <input type="checkbox" class="checkbox" aria-label="Seleccionar todas"></th>
    <th scope="col" aria-sort="ascending">
      <button class="th-sort" type="button">Proyecto
        <svg class="sort-icon" …><path d="m18 15-6-6-6 6"/></svg>
      </button></th>
    <th scope="col" class="num">Ingresos</th>
  </tr></thead>
  <tbody>
    <tr><td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar API de pagos"></td>…</tr>
    <tr class="row--selected">…</tr>
  </tbody>
</table>
<!-- El icono se atenúa si aria-sort no está fijado; JS reordena y alterna aria-sort. -->

Stat

Cifra de dashboard con count-up de 350ms al entrar en pantalla (respeta prefers-reduced-motion).

Ingresos del mes $0 ▲ +12,4 %
Usuarios activos 0 ▼ −3,1 %
Uptime 0 % — sin cambios
Ver código
<div class="stat">
  <span class="stat__label">Ingresos del mes</span>
  <span class="stat__value">$128 450</span>
  <span class="stat__delta stat__delta--up">▲ +12,4 %</span>
</div>

Pagination

1-indexado, bajo tablas y listas largas · trunca con elipsis a partir de 7 páginas · página activa con fondo --accent-400 + peso 700 (no depende solo del color) y aria-current="page".

    Ver código
    <ul class="pagination" aria-label="Paginación">
      <li><button class="pagination__btn" data-page="prev" aria-label="Página anterior">‹</button></li>
      <li><button class="pagination__btn" data-page="1">1</button></li>
      <li><span class="pagination__ellipsis">…</span></li>
      <li><button class="pagination__btn is-active" data-page="5" aria-current="page">5</button></li>
      <li><button class="pagination__btn" data-page="6">6</button></li>
      <li><button class="pagination__btn" data-page="next" aria-label="Página siguiente">›</button></li>
    </ul>

    Navegación

    Navbar (blur 12px)

    Sidebar

    Tabs y Breadcrumbs

    Ver código
    <div class="navbar">…<a class="navbar__link is-active">Resumen</a>…</div>
    <div class="sidebar"><a class="sidebar__item is-active"><svg>…</svg>Inicio</a></div>
    <div class="tabs"><button class="tabs__btn is-active">Resumen</button>…<span class="tabs__indicator"></span></div>
    <ul class="breadcrumbs"><li><a>Inicio</a></li><li aria-current="page">Actual</li></ul>

    Skeleton y EmptyState

    Aún no hay proyectos Crea tu primer proyecto para empezar a ver métricas aquí.
    Ver código
    <span class="skeleton" style="height:12px;width:70%"></span>
    <span class="skeleton skeleton--circle" style="width:40px;height:40px"></span>
    
    <div class="empty-state">
      <svg>…</svg>
      <span class="empty-state__title">Aún no hay proyectos</span>
      <span class="empty-state__desc">Crea tu primer proyecto…</span>
      <button class="btn btn--primary btn--sm">Crear proyecto</button>
    </div>

    Card, Link y Code

    Card

    Tarjeta en reposo

    Superficie 1, borde sutil, sombra nivel 1.

    Tarjeta interactiva

    Al pasar el cursor sube de nivel: −2px + superficie más clara.

    Ver código
    <div class="card">…</div>
    <div class="card card--interactive">…</div>
    <!-- .card--glow: firma de marca — MÁXIMO UNO POR PANTALLA -->

    Link y Code

    Link con subrayado animado Link externo Acento con var(--accent-400)
    Ver código
    <a class="link" href="…">Link con subrayado animado</a>
    <span class="code">var(--accent-400)</span>

    Combobox y Search

    Combobox con autocompletado: filtra en vivo y se navega con ↑/↓, Enter selecciona, Esc cierra. El listado vive sobre --surface-2. Al lado, un campo de búsqueda con botón para limpiar.

    Framework
    Buscar
    Ver código
    <div class="combobox">
      <input class="input combobox__input" role="combobox" aria-expanded="false"
        aria-controls="lista" aria-autocomplete="list" aria-activedescendant="">
      <ul class="combobox__list" id="lista" role="listbox" aria-label="Frameworks" hidden>
        <li class="combobox__option" role="option" id="op-0" aria-selected="false">Angular</li>
        <!-- .is-active = resaltado por teclado · aria-selected="true" = elegida -->
      </ul>
    </div>
    
    <div class="search">
      <svg class="search__icon" aria-hidden="true">…lupa…</svg>
      <input class="input search__input" type="search" aria-label="Buscar">
      <button class="search__clear" aria-label="Limpiar" hidden>×</button>
    </div>
    <!-- JS: filtra en vivo, ↑/↓/Enter/Esc; el clear vacía y devuelve el foco. -->

    Drawer

    Panel que entra deslizándose desde un borde (derecha por defecto; --left / --bottom). Igual que el modal: role="dialog" + aria-modal, foco atrapado, retorno de foco, y cierre con Esc o clic en el overlay. El panel usa overscroll-behavior: contain.

    Ver código
    <div class="drawer drawer--right" hidden>
      <div class="drawer__overlay" data-close-drawer></div>
      <aside class="drawer__panel" role="dialog" aria-modal="true"
             aria-labelledby="dTitle" tabindex="-1">
        <div class="drawer__header">
          <h3 class="drawer__title" id="dTitle">Filtros</h3>
          <button class="icon-btn icon-btn--ghost" data-close-drawer aria-label="Cerrar">×</button>
        </div>
        <div class="drawer__body">…</div>
        <div class="drawer__footer">…</div>
      </aside>
    </div>
    <!-- Variantes: --right (default) · --left · --bottom (sheet).
         JS: .is-open dispara el slide; Esc / overlay cierran y devuelven el foco. -->

    Popover

    Se abre por clic —eso lo distingue del Tooltip, que es hover— y crece desde el transform-origin del disparador. role="dialog", el foco entra al panel; cierra con Esc o clic fuera. Panel sobre --surface-2.

    Ver código
    <div class="popover">
      <button class="btn btn--outline popover__trigger" type="button"
              aria-haspopup="dialog" aria-expanded="false" aria-controls="panel">Compartir</button>
      <div class="popover__panel" role="dialog" aria-label="Compartir proyecto" id="panel">
        <p>Contenido de varias líneas…</p>
        <button class="btn btn--primary btn--sm">Copiar</button>
        <a class="link" href="…">Ver permisos avanzados</a>
      </div>
    </div>
    <!-- Abre por CLICK (no hover); el foco entra al panel; Esc / clic fuera cierran. -->

    ButtonGroup

    Botones adyacentes con bordes compartidos: el radio queda solo en los extremos y los bordes internos se colapsan. Reutiliza los .btn existentes. role="group" + aria-label.

    Ver código
    <div class="btn-group" role="group" aria-label="Alineación del texto">
      <button class="btn btn--outline">Izquierda</button>
      <button class="btn btn--outline">Centro</button>
      <button class="btn btn--outline">Derecha</button>
    </div>
    <!-- Radios solo en los extremos; bordes internos colapsados (margin-left:-1px). -->

    DatePicker

    Campo de fecha con calendario en popover: el input abre el calendario al hacer clic, eliges un día con el ratón o las flechas y el valor vuelve al campo. El panel reutiliza el look de .popover__panel sobre --surface-2. El .calendar__grid es role="grid" y cada día un <button role="gridcell"> con la fecha completa en aria-label.

    Fecha de entrega
    Ver código
    <div class="datepicker">
      <div class="input-wrap">
        <svg aria-hidden="true">…calendario…</svg>
        <input class="input datepicker__input" readonly aria-haspopup="dialog"
               aria-expanded="false" aria-controls="cal" value="15 de julio de 2026">
      </div>
      <div class="datepicker__calendar" id="cal" role="dialog" aria-label="Elegir fecha" hidden>
        <div class="calendar">
          <div class="calendar__header">
            <button class="icon-btn icon-btn--ghost icon-btn--sm" aria-label="Mes anterior">‹</button>
            <span class="calendar__title" aria-live="polite">julio 2026</span>
            <button class="icon-btn icon-btn--ghost icon-btn--sm" aria-label="Mes siguiente">›</button>
          </div>
          <div class="calendar__weekdays" role="row">
            <span role="columnheader" aria-label="lunes">L</span> …
          </div>
          <div class="calendar__grid" role="grid">
            <div class="calendar__week" role="row">
              <button class="calendar__day" role="gridcell"
                      aria-label="15 de julio de 2026" aria-selected="true">15</button> …
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- JS: clic abre; flechas mueven el día (±1, ±7) cruzando meses; Enter elige; Esc cierra. -->

    FileUpload

    Zona de arrastre con lista de archivos. El input file real va oculto accesiblemente (.visually-hidden, sigue tabulable) y el ring de foco se pinta sobre el label. Al arrastrar un archivo encima, .is-dragover tiñe el borde de acento. Un ítem subiendo incrusta el .progress; el error se marca con icono + texto, no solo por color.

    • informe-trimestral-q3.pdf 2.4 MB · subiendo…
    • mockup-portada.png 840 KB · completo
    • video-demo.mov Error: supera los 10 MB
    Ver código
    <div class="file-upload">
      <input class="file-upload__input visually-hidden" type="file" id="f" multiple
             aria-describedby="hint">
      <label class="file-upload__label" for="f">
        <svg aria-hidden="true">…subir…</svg>
        <span>Arrastra archivos aquí o <span class="file-upload__button">explora</span></span>
        <span class="file-upload__hint" id="hint">PNG, JPG o PDF · máximo 10 MB</span>
      </label>
      <ul class="file-upload__list">
        <li class="file-upload__item">
          <svg aria-hidden="true">…archivo…</svg>
          <div>
            <span class="file-upload__name">informe.pdf</span>
            <span class="file-upload__size">2.4 MB · subiendo…</span>
            <div class="progress" role="progressbar" aria-valuenow="60" …>
              <div class="progress__bar" style="width:60%"></div></div>
          </div>
          <button class="file-upload__remove" aria-label="Cancelar informe.pdf">×</button>
        </li>
        <li class="file-upload__item file-upload__item--error">…icono + texto de error…</li>
      </ul>
    </div>
    <!-- JS: dragover añade .is-dragover; el botón × quita el ítem. -->

    Stepper / Wizard

    Indicador de progreso por pasos. Es un <ol>, así que el orden y el conteo se anuncian solos; el paso en curso lleva aria-current="step" y los pasos completos muestran un check (estado no solo por color). El conector se tiñe de acento en el tramo ya recorrido. Variante .stepper--vertical para apilarlos.

    1. Completado: Cuenta
    2. Completado: Perfil
    3. Paso actual: 3 Pago
    4. Pendiente: 4 Confirmación
    1. Completado: Compilar el proyecto
    2. Paso actual: 2 Ejecutar pruebas
    3. Pendiente: 3 Publicar en producción
    Ver código
    <ol class="stepper" aria-label="Progreso de la configuración">
      <li class="step step--complete">
        <span class="step__marker"><svg aria-hidden="true">…check…</svg></span>
        <span class="step__label">Cuenta</span>
        <span class="step__connector" aria-hidden="true"></span>
      </li>
      <li class="step step--current" aria-current="step">
        <span class="step__marker">3</span>
        <span class="step__label">Pago</span>
        <span class="step__connector" aria-hidden="true"></span>
      </li>
      <li class="step step--upcoming">
        <span class="step__marker">4</span><span class="step__label">Confirmación</span>
      </li>
    </ol>
    <!-- El último paso no lleva conector. Variante: .stepper--vertical. -->

    Kbd

    Tecla o atajo de teclado sobre <kbd>: JetBrains Mono, borde --border-strong y sombra sutil, alto y ancho mínimo 20px. Para combos se encadenan varias con un "+" literal. Es informativa, nunca un botón.

    Esc Tab + K Ctrl + S Shift +
    Ver código
    Pulsa <kbd class="kbd">Esc</kbd> para cerrar.
    
    Abre el buscador con <kbd class="kbd">⌘</kbd> + <kbd class="kbd">K</kbd>.
    Guarda con <kbd class="kbd">Ctrl</kbd> + <kbd class="kbd">S</kbd>.

    Motion

    Solo transform y opacity. Salida ~20% más corta que la entrada. Distancias de 4–8px. Stagger 30ms/ítem (máximo 6).

    TokenValorUso
    --duration-instant100mspress, toggle
    --duration-fast150mshover, focus, color
    --duration-base220msdropdowns, tooltips, switches
    --duration-slow250msmodales, toasts
    --duration-page350mstransiciones de página
    --ease-outcubic-bezier(0.2, 0, 0, 1)entradas (default)
    --ease-incubic-bezier(0.4, 0, 1, 1)salidas
    --ease-in-outcubic-bezier(0.65, 0, 0.35, 1)moverse de A a B (indicador de tabs)
    --ease-springcubic-bezier(0.34, 1.4, 0.64, 1)toggles, checks
    entrada
    fade + glow

    Firma de marca: los elementos con acento entran con fade + glow — solo en el elemento hero/CTA. prefers-reduced-motion colapsa todo a 0.01ms.

    Accesibilidad

    CombinaciónContrasteVeredicto
    night-100 sobre night-95017.25:1AAA
    accent-400 sobre night-9509.50:1AA — sirve como texto/link
    night-950 sobre accent-400 (CTA)9.50:1AA — texto del CTA oscuro
    indigo-500 sobre night-9502.09:1Solo fondo, nunca texto
    night-50 sobre indigo-5009.29:1AAA
    accent-600 sobre blanco (modo claro)4.01:1Solo UI — nunca texto
    accent-700 sobre blanco (texto de acción en claro)5.95:1AA
    accent-800 sobre blanco (hover en claro)8.74:1AAA
    info sobre info-bg (badge en claro)5.20:1AA

    Tokens y descarga

    El sistema se distribuye en dist/nocturna-electrica/: nocturna-electrica.css (framework completo), tokens.css (solo variables), tailwind.css (bloque @theme para Tailwind v4) y tokens.json (formato estándar W3C DTCG 2025.10, interoperable con Figma / Tokens Studio / Style Dictionary).

    Variables CSS (núcleo)

    tokens :root (aliases semánticos)
    :root {
      /* Superficies */
      --bg: #010203;
      --surface-1: #0b0d12; --surface-2: #14171f; --surface-3: #1e222d;
      --border: #3d4354; --border-strong: #2a2f3d;
    
      /* Texto */
      --text-primary: #e8eaef; --text-secondary: #c5c9d4;
      --text-muted: #9aa0b0; --text-disabled: #6b7183;
      --text-heading: #f6f7f9; --text-on-accent: #010203;
    
      /* Acento e índigo */
      --accent-400: #52baed; --accent-500: #2fa3dd; --accent-600: #1f87bb;
      --indigo-500: #353d7c; --indigo-700: #22284f; --indigo-800: #1a1e3b;
      --accent-ghost-hover: rgba(82,186,237,.08);
      --accent-selected: rgba(82,186,237,.12);
    
      /* Semánticos */
      --success: #4ade80; --warning: #fbbf24; --danger: #f87171; --info: #52baed;
    
      /* Tipografía */
      --font-display: "Space Grotesk", system-ui, sans-serif;
      --font-body: "Manrope", system-ui, sans-serif;
      --font-mono: "JetBrains Mono", monospace;
    
      /* Radios y elevación */
      --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-full: 9999px;
      --shadow-1: 0 1px 2px rgba(0,0,0,.4);
      --shadow-2: 0 4px 12px rgba(0,0,0,.5);
      --shadow-3: 0 12px 32px rgba(0,0,0,.6);
      --glow-accent: 0 0 0 1px rgba(82,186,237,.35), 0 4px 20px rgba(82,186,237,.18);
      --ring: 0 0 0 3px rgba(82,186,237,.2);
    
      /* Motion */
      --duration-fast: 150ms; --duration-base: 220ms; --duration-slow: 250ms;
      --ease-out: cubic-bezier(0.2, 0, 0, 1);
      --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
      --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
    }

    Tailwind CSS v4 — @theme

    tailwind.css
    @import "tailwindcss";
    
    @theme {
      --color-night-950: #010203; --color-night-900: #0b0d12;
      --color-night-800: #14171f; --color-night-700: #1e222d;
      --color-night-600: #2a2f3d; --color-night-500: #3d4354;
      --color-night-400: #6b7183; --color-night-300: #9aa0b0;
      --color-night-200: #c5c9d4; --color-night-100: #e8eaef;
      --color-night-50: #f6f7f9;
      --color-accent-100: #d9f0fb; --color-accent-200: #a8ddf6;
      --color-accent-300: #7dccf1; --color-accent-400: #52baed;
      --color-accent-500: #2fa3dd; --color-accent-600: #1f87bb;
      --color-accent-700: #186a94; --color-accent-800: #12506f;
      --color-indigo-300: #7a83c4; --color-indigo-400: #565fa3;
      --color-indigo-500: #353d7c; --color-indigo-600: #2b3264;
      --color-indigo-700: #22284f; --color-indigo-800: #1a1e3b;
      --color-success: #4ade80; --color-warning: #fbbf24;
      --color-danger: #f87171; --color-info: #52baed;
    
      --font-display: "Space Grotesk", system-ui, sans-serif;
      --font-body: "Manrope", system-ui, sans-serif;
      --font-mono: "JetBrains Mono", monospace;
    
      --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 24px;
    
      --shadow-1: 0 1px 2px rgba(0,0,0,.4);
      --shadow-2: 0 4px 12px rgba(0,0,0,.5);
      --shadow-3: 0 12px 32px rgba(0,0,0,.6);
      --shadow-glow: 0 0 0 1px rgba(82,186,237,.35), 0 4px 20px rgba(82,186,237,.18);
    
      --ease-out: cubic-bezier(0.2, 0, 0, 1);
      --ease-in: cubic-bezier(0.4, 0, 1, 1);
      --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
      --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
    }
    Fuente de verdad completa: DESIGN-nocturna-electrica.md. Skill de Claude: ~/.claude/skills/nocturna-electrica-design/.

    Instalación

    El sistema se distribuye como paquete npm (@higinio/design-css) y también como archivos sueltos en dist/nocturna-electrica/. Instálalo o cópialos, y elige qué importar según tu stack (A/B/C/D). En todas: dark por defecto; <html data-theme="light"> activa el modo claro, y los componentes consumen solo aliases semánticos, nunca hex.

    Instalar desde npm (recomendado)

    Un solo paquete con los dos sistemas en subrutas /celeste/ y /nocturna/. Instálalo y usa las vías de abajo apuntando a la subruta, en vez de a un archivo copiado.

    terminal + CSS de entrada
    npm i @higinio/design-css
    
    /* framework completo (vía A) */
    @import "@higinio/design-css/nocturna/framework.css";
    /* o solo tokens (vía B) */
    @import "@higinio/design-css/nocturna/tokens.css";
    /* o Tailwind v4 (vía C) */
    @import "@higinio/design-css/nocturna/tailwind.css";

    A · CSS drop-in (framework completo)

    Para cualquier proyecto — HTML plano o cualquier framework. Trae variables + clases de componentes listas (tipo Bootstrap) y ya importa las fuentes.

    index.html
    <link rel="stylesheet" href="/estilos/nocturna-electrica.css">
    <!-- luego usas las clases directo: -->
    <button class="btn btn--primary">Guardar</button>
    <div class="card">…</div>

    B · Solo tokens

    Si diseñas tus propios componentes: solo las variables, sin clases.

    styles.css
    @import "./tokens.css";
    
    .mi-panel {
      background: var(--surface-1);
      color: var(--text-primary);
      border: 1px solid var(--border);
    }

    C · Tailwind v4

    Tailwind v4 se configura por CSS. El tailwind.css ya incluye @import "tailwindcss", los tokens y el bloque @theme — úsalo como tu hoja de entrada (mantén tokens.css a su lado, que es lo que importa).

    terminal + CSS de entrada
    # 1) instala Tailwind v4
    npm i tailwindcss @tailwindcss/postcss
    
    # 2) copia tailwind.css + tokens.css a tu carpeta de CSS
    #    y usa tailwind.css como entrada. Ya tienes utilidades:
    <div class="bg-night-900 text-accent-400">…</div>

    D · Pipelines de tokens

    Para Figma, Tokens Studio o Style Dictionary: importa tokens.json (formato estándar W3C DTCG 2025.10).

    Con Claude no copias nada: invoca la skill nocturna-electrica-design o conecta el MCP (sección siguiente) y construye consumiendo tokens, componentes y reglas del sistema, con validación automática.

    MCP

    El paquete @higinio/design-mcp expone ambos sistemas de diseño — Celeste Clásica y Nocturna Eléctrica — a cualquier IDE o cliente compatible con Model Context Protocol: tokens, componentes, reglas duras y validación de código. Complementa a las skills de Claude, no las reemplaza: misma fuente de verdad, accesible también fuera del entorno de Claude.

    Las 8 tools

    ToolQué hace
    list_design_systemsLista los sistemas de diseño disponibles.
    get_tokenDevuelve el valor resuelto de un token o alias de un sistema.
    search_tokensBusca tokens por nombre, categoría o valor.
    list_componentsLista los componentes documentados de un sistema.
    get_component_specDevuelve la especificación completa de un componente: estados, medidas, variantes.
    get_rulesDevuelve las reglas duras (no negociables) del sistema.
    validate_codeAudita CSS/HTML contra las reglas del sistema — hex directos, contraste, aliases.
    generate_snippetGenera un snippet de componente listo para usar, con los aliases correctos.

    Conectar (remoto)

    Añade el servidor a tu .mcp.json:

    .mcp.json
    {
      "mcpServers": {
        "design": {
          "type": "http",
          "url": "https://design-mcp.higiniolab.tech/mcp",
          "headers": { "Authorization": "Bearer <tu-token>" }
        }
      }
    }

    Ejemplo

    Consultar un token de este sistema:

    get_token
    get_token(system="nocturna", name="--accent")
    → "#52baed"

    Auditar código antes de integrarlo:

    validate_code
    validate_code(system="nocturna", code="…")
    → { ok: false, issues: ["hex directo #52baed en línea 12 — usa var(--accent-400)"] }
    El MCP y las skills comparten fuente de verdad. Usa las skills dentro de Claude; el MCP para IDEs y clientes MCP externos.