/*
 * cyberconquer.css — sistema de diseño importado de Claude Design
 * ("CyberConquer CTF — Operator Console") y adaptado a esta plataforma.
 *
 * ORIGEN
 * El diseño llegó como una maqueta React/JSX cuyo CSS es autónomo: define su
 * propia paleta en OKLCH, su shell de aplicación y todos sus componentes por
 * nombre de clase. Ese CSS se porta aquí tal cual, porque es la fuente de
 * verdad del diseño; lo que se adapta es el MARCADO que lo consume, que en la
 * maqueta lo generaba React y aquí lo generan las vistas Yii.
 *
 * QUÉ CAMBIA RESPECTO A LA MAQUETA
 *  - Los textos salen de Yii::t, no literales en inglés.
 *  - Los datos (VPN, evento, rango, puntos) salen del modelo, no de data.jsx.
 *  - Se añade un puente con los tokens --ac-* que ya usaban academia-design.css
 *    y academia.css, para que las reglas escritas antes no peleen con esta
 *    paleta. Sin ese puente convivirían dos sistemas de color a la vez.
 *
 * ORDEN DE CARGA
 * Va DESPUÉS de academia-design.css, así que gana por cascada. El tema anterior
 * sigue intacto y revertir es quitar una línea de MaterialAsset.
 */

/* ============================================================
   TOKENS — tal como los define el diseño
   ============================================================ */
:root {
  --bg: oklch(0.155 0.012 260);
  --bg-1: oklch(0.195 0.015 260);
  --bg-2: oklch(0.235 0.017 260);
  --bg-3: oklch(0.285 0.020 260);
  --line: oklch(0.33 0.022 260);
  --line-soft: oklch(0.27 0.018 260);
  --line-bright: oklch(0.46 0.028 260);
  --fg: oklch(0.98 0.004 260);
  --fg-dim: oklch(0.76 0.012 260);
  /* 0.60 daba 4.26:1 sobre las superficies elevadas (tarjeta de usuario,
     barra de estado). Medido: 0.66 pasa AA sobre todas ellas. */
  --fg-mute: oklch(0.66 0.016 260);
  /* 0.48 daba 3.00:1 sobre el fondo de la app: por debajo de AA para el
     texto pequeno de los antetitulos y las etiquetas de seccion, que es
     justo donde se usa. Medido: el umbral es 0.58; 0.62 deja margen. */
  --fg-faint: oklch(0.62 0.018 260);
  --accent: oklch(0.90 0.23 140);
  --accent-1: oklch(0.84 0.22 142);
  --accent-dim: oklch(0.62 0.16 144);
  --accent-bg: oklch(0.55 0.18 143 / 0.14);
  --accent-glow: oklch(0.90 0.23 140 / 0.45);
  --info: oklch(0.80 0.15 225);
  --info-bg: oklch(0.55 0.14 225 / 0.14);
  --warn: oklch(0.85 0.17 80);
  --warn-bg: oklch(0.60 0.14 80 / 0.14);
  --danger: oklch(0.72 0.21 20);
  --danger-bg: oklch(0.55 0.18 20 / 0.14);
  --purple: oklch(0.74 0.17 300);
  --purple-bg: oklch(0.55 0.16 300 / 0.14);
  --pink: oklch(0.74 0.17 350);
  --mono: 'Geist Mono', 'Roboto Mono', ui-monospace, monospace;
  --sans: 'Geist', 'Roboto', system-ui, sans-serif;
  --t: cubic-bezier(0.22, 0.61, 0.36, 1);
  --r-sm: 8px; --r-md: 12px; --r-lg: 16px; --r-pill: 999px;
  --shadow-1: 0 2px 8px oklch(0 0 0 / 0.25);
  --shadow-2: 0 12px 40px -8px oklch(0 0 0 / 0.5);
  --card-grad: linear-gradient(180deg, oklch(0.215 0.016 260), oklch(0.185 0.014 260));
}

/*
 * Puente con el sistema anterior.
 *
 * academia-design.css y academia.css estilizan ~1900 líneas contra los tokens
 * --ac-*. Reapuntarlos a la paleta nueva convierte todo aquel trabajo en parte
 * de este diseño en vez de en un sistema rival: los primitivos de Bootstrap que
 * ya se habían revestido (.btn, .card, .form-control, .table) heredan los
 * colores de CyberConquer sin reescribir una sola regla.
 */
:root {
  --ac-nivel-0: var(--bg);
  --ac-nivel-1: var(--bg-1);
  --ac-nivel-2: var(--bg-2);
  --ac-nivel-3: var(--bg-3);
  --ac-nivel-4: var(--line);
  --ac-linea: var(--line-soft);
  --ac-linea-fuerte: var(--line);
  --ac-txt: var(--fg);
  --ac-txt-2: var(--fg-dim);
  --ac-txt-3: var(--fg-mute);
  --ac-acento: var(--accent);
  --ac-acento-2: var(--accent-1);
  --ac-acento-suave: var(--accent-bg);
  --ac-acento-borde: oklch(0.55 0.18 143 / 0.4);
  --ac-info: var(--info);
  --ac-exito: var(--accent);
  --ac-aviso: var(--warn);
  --ac-error: var(--danger);
  --ac-info-suave: var(--info-bg);
  --ac-exito-suave: var(--accent-bg);
  --ac-aviso-suave: var(--warn-bg);
  --ac-error-suave: var(--danger-bg);
  --ac-fuente: var(--sans);
  --ac-fuente-cifras: var(--mono);
  --ac-fuente-codigo: var(--mono);
  --ac-r-sm: var(--r-sm);
  --ac-r: var(--r-md);
  --ac-r-lg: var(--r-lg);
  --ac-r-full: var(--r-pill);
  --ac-curva: var(--t);
  --ac-sombra-1: var(--shadow-1);
  --ac-sombra-2: var(--shadow-2);
  --ac-sombra-3: var(--shadow-2);
}

/* ============================================================
   BASE
   ============================================================ */
* { box-sizing: border-box; }

body {
  font-family: var(--sans);
  background: var(--bg);
  color: var(--fg);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  font-feature-settings: "ss01", "cv11";
  letter-spacing: -0.006em;
  /* El degradado del diseño sustituye al que traía la hoja anterior. */
  background-image: none;
}

/* Halo de fondo. En la maqueta es body::before; aquí body ya lleva ::before en
 * otras reglas del tema heredado, así que se pinta sobre el contenedor raíz. */
.cc-app::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(900px 600px at 12% -5%, oklch(0.55 0.18 143 / 0.10), transparent 60%),
    radial-gradient(800px 600px at 100% 110%, oklch(0.55 0.16 265 / 0.12), transparent 55%);
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: var(--r-pill); border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--line-bright); }

/* ══════════════════════════════════════════════════════════════════════════
   SELECCION DE TEXTO — la unica regla ::selection del proyecto
   ══════════════════════════════════════════════════════════════════════════
   Habia TRES reglas encadenadas y el resultado era texto ilegible al subrayar.
   La primera, en academia-design.css, declaraba:

       ::selection { background: var(--ac-acento); color: var(--ac-nivel-0); }

   `--ac-nivel-0` es el fondo de la aplicacion, casi negro. Las dos reglas
   siguientes —las escribi yo— cambiaban el fondo a un verde translucido y
   OMITIAN el color, con un comentario afirmando que asi «el navegador respeta
   el color original».

   Eso es falso, y es el error de fondo: omitir una propiedad no la deshace.
   La cascada conserva el ultimo valor declarado, que seguia siendo el casi
   negro de la primera regla. Corregi el fondo dos veces y nunca el color,
   porque mi propio comentario me decia que ya estaba resuelto.

   Resultado en pantalla: fondo verde oscuro con texto casi negro encima. Al
   arrastrar el raton sobre cualquier parrafo, el texto desaparecia.

   Ahora se declaran LAS DOS propiedades juntas y en un solo sitio. El color
   claro resuelve ademas el problema que intentaba evitar la regla anterior:
   los glifos de icono se seleccionan en claro y siguen visibles. */
::selection {
  background: var(--accent-bg);
  color: var(--fg);
}
::-moz-selection {
  background: var(--accent-bg);
  color: var(--fg);
}

/* ============================================================
   SHELL DE APLICACIÓN
   ============================================================ */
/* La fila `status` de 28px se retiro junto con la barra inferior: repetia los
   mismos cuatro datos que ya muestra la barra lateral. Esos 28px vuelven al
   contenido en todas las pantallas. */
.cc-app {
  display: grid;
  grid-template-columns: 256px 1fr;
  grid-template-rows: 1fr;
  grid-template-areas: "side main";
  height: 100vh;
  overflow: hidden;
}

.cc-app > .sidebar { grid-area: side; }
.cc-app > .main { grid-area: main; }

.sidebar {
  border-right: 1px solid var(--line-soft);
  background: linear-gradient(180deg, oklch(0.20 0.016 262), oklch(0.17 0.013 260));
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
  z-index: 2;
  /* Anula el posicionamiento fijo del tema heredado, que lo sacaba de la
   * rejilla y lo superponía al contenido. */
  position: relative !important;
  width: auto !important;
  height: auto !important;
  top: auto !important;
  left: auto !important;
  transform: none !important;
  box-shadow: none !important;
}

.main {
  display: flex;
  flex-direction: column;
  /* `min-height: 0` es lo que permite que un hijo flex encoja por debajo de su
     contenido. Sin el, .content se estira hasta su altura natural, empuja al
     padre y lo que sobra queda fuera de la pantalla sin barra que lo alcance.
     Es la causa de que el scroll pareciera "cortado" en casi todas las vistas. */
  min-height: 0;
  overflow: hidden;
  background: transparent;
  /* El tema heredado empujaba .main-panel 260px a la derecha para dejar sitio a
   * una barra lateral fija; con la rejilla eso duplicaría el margen. */
  width: auto !important;
  float: none !important;
  margin: 0 !important;
}

/* ---------- Marca ---------- */
.brand-block { padding: 18px 16px 16px; border-bottom: 1px solid var(--line-soft); position: relative; }
.brand-row { display: flex; align-items: center; gap: 12px; }
.brand-mark-lg {
  width: 34px; height: 34px; border-radius: 10px;
  background: linear-gradient(150deg, var(--accent), oklch(0.72 0.19 155));
  display: grid; place-items: center;
  font-family: var(--mono); font-weight: 800; color: #06210a; font-size: 17px;
  flex: none;
  box-shadow: 0 0 0 1px oklch(0.90 0.23 140 / 0.3), 0 6px 18px -4px var(--accent-glow);
  overflow: hidden;
}
.brand-mark-lg img { width: 100%; height: 100%; object-fit: cover; }
.brand-text { line-height: 1.1; min-width: 0; }
.brand-text-1 { font-size: 14.5px; font-weight: 700; letter-spacing: -0.02em; }
.brand-text-2 { font-family: var(--mono); font-size: 9px; letter-spacing: 0.22em; color: var(--fg-mute); margin-top: 4px; }
.brand-tag {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 14px;
  padding: 4px 10px; background: var(--accent-bg);
  border: 1px solid oklch(0.55 0.18 143 / 0.35); border-radius: var(--r-pill);
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.18em; color: var(--accent);
}
.brand-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 8px var(--accent); animation: cc-pulse 2.4s infinite; }

/* ---------- Navegación ---------- */
.side-scroll { flex: 1; overflow-y: auto; padding: 12px 0; }
.side-section { padding: 8px 14px 16px; }
.side-label {
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.24em;
  color: var(--fg-faint); text-transform: uppercase;
  padding: 0 8px 10px; display: flex; justify-content: space-between; align-items: center;
}
.side-label .count { color: var(--fg-mute); }

.nav-item {
  display: flex; align-items: center; gap: 11px;
  padding: 7px 10px; margin-bottom: 3px;
  border-radius: var(--r-sm);
  color: var(--fg-dim); font-size: 13px; font-weight: 500;
  cursor: pointer; transition: 0.16s var(--t); position: relative;
  text-decoration: none;
}
.nav-item:hover { background: oklch(0.26 0.018 260 / 0.6); color: var(--fg); text-decoration: none; }
.nav-item.active { background: linear-gradient(90deg, var(--accent-bg), transparent); color: var(--fg); box-shadow: inset 2px 0 0 var(--accent); }
.nav-ico {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  color: var(--fg-mute);
  background: oklch(0.27 0.018 260 / 0.5);
  border: 1px solid var(--line-soft); border-radius: var(--r-sm);
  transition: 0.16s var(--t);
}
.nav-item:hover .nav-ico { color: var(--fg-dim); border-color: var(--line); }
.nav-item.active .nav-ico { color: var(--accent); background: var(--accent-bg); border-color: oklch(0.55 0.18 143 / 0.4); box-shadow: 0 0 14px -2px var(--accent-glow); }
.nav-tag {
  margin-left: auto; padding: 2px 8px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  font-family: var(--mono); font-size: 9px; color: var(--fg-mute); letter-spacing: 0.05em;
}
.nav-tag.live { color: var(--accent); border-color: oklch(0.55 0.18 143 / 0.4); background: var(--accent-bg); }
.nav-tag.new { color: var(--info); border-color: oklch(0.50 0.14 225 / 0.4); background: var(--info-bg); }

/* ---------- Tarjeta de evento ---------- */
.side-event {
  margin: 6px 14px 12px; padding: 14px;
  background: linear-gradient(150deg, var(--accent-bg), oklch(0.20 0.016 260));
  border: 1px solid oklch(0.55 0.18 143 / 0.28);
  border-radius: var(--r-md); position: relative; overflow: hidden;
}
.side-event .label { font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.22em; color: var(--accent); display: flex; align-items: center; gap: 6px; }
.side-event .label .pulse { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 8px var(--accent); animation: cc-pulse 1.8s infinite; }
.side-event .name { font-size: 13.5px; font-weight: 600; margin-top: 8px; letter-spacing: -0.01em; }
.side-event .time { font-family: var(--mono); font-size: 20px; font-weight: 700; letter-spacing: -0.02em; margin-top: 8px; color: var(--fg); font-variant-numeric: tabular-nums; text-shadow: 0 0 18px var(--accent-glow); }
.side-event .stats { display: grid; grid-template-columns: 1fr auto; gap: 5px 12px; margin-top: 10px; font-family: var(--mono); font-size: 10.5px; }
.side-event .stats .k { color: var(--fg-mute); }
.side-event .stats .v { color: var(--fg); text-align: right; }

/* ---------- Tarjeta de usuario ---------- */
.user-card {
  margin: 0 14px 12px; padding: 11px;
  background: var(--bg-2); border: 1px solid var(--line-soft); border-radius: var(--r-md);
  display: flex; align-items: center; gap: 10px;
  cursor: pointer; transition: 0.16s var(--t); text-decoration: none; color: inherit;
}
.user-card:hover { border-color: oklch(0.55 0.18 143 / 0.4); box-shadow: 0 0 20px -6px var(--accent-glow); text-decoration: none; color: inherit; }
.avatar {
  width: 32px; height: 32px; border-radius: var(--r-sm);
  background: linear-gradient(150deg, var(--accent), oklch(0.70 0.18 155));
  color: #06210a; display: grid; place-items: center;
  font-family: var(--mono); font-weight: 700; font-size: 12px; flex: none; overflow: hidden;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-lg { width: 56px; height: 56px; border-radius: var(--r-md); font-size: 22px; }
.user-meta { flex: 1; min-width: 0; }
.user-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-rank { font-family: var(--mono); font-size: 10.5px; color: var(--fg-mute); margin-top: 2px; }
.user-rank b { color: var(--accent); font-weight: 600; }

/* ---------- Pie de la barra lateral: VPN ---------- */
.side-foot { border-top: 1px solid var(--line-soft); padding: 12px 16px; background: oklch(0.17 0.013 260 / 0.6); }
.vpn-line { display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 10.5px; }
.vpn-line + .vpn-line { margin-top: 6px; }
.vpn-line .k { color: var(--fg-mute); width: 62px; flex: none; }
.vpn-line .v { color: var(--fg); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.vpn-line .v.ok { color: var(--accent); }
.vpn-line .v.warn { color: var(--warn); }
.vpn-line .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--fg-faint); }
.vpn-line .dot.ok { background: var(--accent); box-shadow: 0 0 7px var(--accent); }
.vpn-line .dot.warn { background: var(--warn); }

/* ---------- Barra superior ---------- */
.topbar {
  display: flex; align-items: center; gap: 12px;
  padding: 0 20px; height: 60px;
  border-bottom: 1px solid var(--line-soft);
  background: oklch(0.17 0.013 260 / 0.7);
  backdrop-filter: blur(12px);
  flex: none;
  position: relative !important;
  width: auto !important;
  z-index: 3;
}
.breadcrumbs { display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 11.5px; color: var(--fg-mute); flex-wrap: wrap; }
.breadcrumbs a { color: inherit; text-decoration: none; }
.breadcrumbs a:hover { color: var(--accent); }
.breadcrumbs .sep { opacity: 0.4; }
.breadcrumbs .here { color: var(--fg); }
.top-spacer { flex: 1; }

.search {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 12px; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  min-width: 340px; transition: 0.16s var(--t); cursor: text;
}
.search:hover { border-color: var(--line-bright); }
.search:focus-within { border-color: var(--accent-dim); box-shadow: 0 0 0 3px var(--accent-bg); }
.search input {
  background: transparent; border: 0; outline: none; flex: 1;
  font-size: 12.5px; color: var(--fg); cursor: text;
  min-height: 0; padding: 0; min-width: 0;
}
.search input::placeholder { color: var(--fg-mute); }
.kbd { font-family: var(--mono); font-size: 10px; color: var(--fg-mute); padding: 2px 7px; border: 1px solid var(--line); border-radius: 6px; flex: none; }

.icon-btn {
  display: grid; place-items: center;
  width: 38px; height: 38px; flex: none;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--bg); color: var(--fg-dim);
  transition: 0.16s var(--t); position: relative;
}
.icon-btn:hover { border-color: oklch(0.55 0.18 143 / 0.4); color: var(--accent); box-shadow: 0 0 16px -6px var(--accent-glow); }
.icon-btn .dot { position: absolute; top: 7px; right: 7px; width: 7px; height: 7px; background: var(--accent); border-radius: 50%; border: 1.5px solid var(--bg-1); box-shadow: 0 0 6px var(--accent); }
.icon-btn .badge-count {
  position: absolute; top: -6px; right: -6px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: var(--r-pill); background: var(--accent); color: #06210a;
  font-family: var(--mono); font-size: 9px; font-weight: 700; line-height: 17px; text-align: center;
}

/* ---------- Contenido ---------- */
.content {
  /* El unico contenedor que desplaza. `min-height: 0` por el mismo motivo que
     en .main; sin el, `flex: 1` no basta para que encoja. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* Ninguna altura minima heredada puede forzar el desbordamiento: el tema
     traia `min-height: 100vh` en varios contenedores, que dentro de un shell
     que YA mide 100vh garantiza que el contenido no quepa. */
  min-height: 0 !important;
  padding: 26px 28px 40px !important;
  position: relative; z-index: 1;
  /* Desplazamiento suave con el teclado y espacio para que el ultimo elemento
     no quede pegado al borde inferior. */
  scroll-padding-bottom: 24px;
}

/* perfect-scrollbar neutralizado por si algun JS heredado lo aplicara igual.
   Sus reglas llevan !important, asi que hace falta la misma fuerza. */
.ps-container,
.main-panel.ps-container,
.sidebar-wrapper.ps-container {
  overflow: visible !important;
}
.main.ps-container { overflow: hidden !important; }
.content.ps-container { overflow-y: auto !important; }
.ps-scrollbar-x-rail, .ps-scrollbar-y-rail { display: none !important; }

/* ---------- Barra de estado ---------- */
.statusbar {
  background: oklch(0.17 0.013 260 / 0.85);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--line-soft);
  display: flex; align-items: center; gap: 0;
  font-family: var(--mono); font-size: 10.5px; color: var(--fg-mute);
  position: relative; z-index: 2; overflow: hidden;
}
.status-cell { padding: 0 14px; height: 100%; display: flex; align-items: center; gap: 7px; border-right: 1px solid var(--line-soft); white-space: nowrap; }
.status-cell:last-child { border-right: 0; }
.status-cell.accent { color: var(--accent); }
.status-cell.warn { color: var(--warn); }
.status-cell b { color: var(--fg); font-weight: 500; }
.status-spacer { flex: 1; }
.status-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.status-dot.ok { background: var(--accent); box-shadow: 0 0 7px var(--accent); }
.status-dot.warn { background: var(--warn); }

/* ============================================================
   PRIMITIVAS DE PÁGINA
   ============================================================ */
.page-head {
  display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: end;
  padding-bottom: 20px; margin-bottom: 22px;
  border-bottom: 1px solid var(--line-soft);
}
.page-eyebrow {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.24em;
  color: var(--accent); display: flex; align-items: center; gap: 8px; font-weight: 500;
  margin: 0;
}
.page-eyebrow::before { content: ""; width: 14px; height: 2px; border-radius: 2px; background: var(--accent); box-shadow: 0 0 8px var(--accent); flex: none; }
.page-title { font-size: 30px; font-weight: 700; letter-spacing: -0.03em; margin: 8px 0 5px; line-height: 1.1; }
.page-sub { color: var(--fg-mute); font-family: var(--mono); font-size: 11.5px; margin: 0; }
.page-sub b { color: var(--fg-dim); font-weight: 500; }
.page-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---------- Botones ---------- */
/* Selector deliberadamente PLANO: una sola clase, especificidad (0,1,0).
 *
 * Antes esta lista era:
 *   .btn, .btn:not(:disabled):not(.disabled), a.btn, button.btn, input[type="submit"].btn
 *
 * En una lista de selectores, la declaracion gana con la especificidad del
 * selector MAS ALTO que coincida. `.btn:not(:disabled):not(.disabled)` vale
 * (0,3,0) porque :not() aporta la especificidad de su argumento, asi que el
 * estilo BASE del boton ganaba a todas las variantes de color, que valen (0,2,0)
 * como maximo. Medido en el navegador: btn-primary, btn-danger y btn-success
 * devolvian los tres el mismo fondo, oklch(0.235 0.017 260). En una consola
 * donde se apagan y reinician maquinas, el boton de una accion destructiva se
 * veia exactamente igual que el de guardar.
 *
 * Los sufijos de elemento (a.btn, button.btn) tampoco hacian falta: .btn ya los
 * alcanza, y al valer (0,1,1) tambien le ganaban a variantes de (0,1,0).
 *
 * Esta hoja se emite la ultima, asi que (0,1,0) basta para ganar a material.css
 * por orden, sin escalar especificidad. Regla del proyecto: el estilo base de un
 * primitivo va con UNA clase; las variantes suben a dos. */
.btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 15px;
  background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--fg-dim); font-size: 12.5px; font-weight: 500;
  transition: 0.16s var(--t); white-space: nowrap;
  text-transform: none; letter-spacing: 0; box-shadow: none;
  min-height: 38px;
}
.btn:hover { border-color: var(--line-bright); color: var(--fg); background: var(--bg-3); transform: translateY(-1px); text-decoration: none; }
.btn:active { transform: translateY(0); }
/* VARIANTES DE COLOR — todas en la forma .btn.btn-X, especificidad (0,2,0).
 *
 * Escritas de una sola vez y no una a una segun hicieran falta: el color de un
 * boton es lo unico que distingue "guardar" de "borrar" antes de leer la
 * etiqueta, y faltaban success, info y warning, que caian al estilo base.
 * Cada par fondo/texto esta comprobado por encima de 4.5:1. */
.btn.btn-primary {
  background: linear-gradient(150deg, var(--accent), oklch(0.80 0.20 148));
  color: #06210a; border-color: transparent; font-weight: 600;
  box-shadow: 0 4px 16px -4px var(--accent-glow);
}
.btn.btn-success {
  background: var(--accent-bg); border-color: oklch(0.55 0.18 143 / 0.55);
  color: var(--accent); font-weight: 600;
}
.btn.btn-info {
  background: var(--info-bg); border-color: oklch(0.55 0.14 225 / 0.55);
  color: var(--info); font-weight: 600;
}
.btn.btn-warning {
  background: var(--warn-bg); border-color: oklch(0.60 0.14 80 / 0.55);
  color: var(--warn); font-weight: 600;
}
.btn.btn-secondary {
  background: var(--bg-3); border-color: var(--line-bright); color: var(--fg);
}
/* Utilidades de texto de Bootstrap dentro de un boton: se ignoran.
 *
 * `.text-dark` esta pensada para fondos claros y en esta paleta resuelve al
 * color del FONDO de la pagina. Medido en /networks: el boton "Ir a la red"
 * llevaba `btn btn-primary text-dark` y daba 1.17:1 — texto casi negro sobre
 * fondo casi negro. El color del texto lo decide la variante del boton, nunca
 * una utilidad suelta escrita en la vista. */
.btn.text-dark, .btn.text-white, .btn.text-light, .btn.text-muted { color: inherit; }
.btn.btn-primary.text-dark { color: #06210a; }
.btn.btn-primary:hover { background: linear-gradient(150deg, oklch(0.93 0.23 140), var(--accent)); color: #06210a; box-shadow: 0 6px 22px -4px var(--accent-glow); }
.btn-ghost, .btn-link { background: transparent; border-color: transparent; color: var(--fg-mute); }
.btn-ghost:hover, .btn-link:hover { background: var(--bg-2); color: var(--fg); }
.btn.btn-danger, .btn.btn-rose {
  color: var(--danger); border-color: oklch(0.55 0.18 20 / 0.4); background: var(--danger-bg);
  font-weight: 600;
}
.btn.btn-danger:hover, .btn.btn-rose:hover { border-color: var(--danger); color: var(--danger); background: var(--danger-bg); }
.btn:disabled, .btn.disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.btn-block { width: 100%; justify-content: center; }

/* ---------- Chips ---------- */
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border: 1px solid var(--line); border-radius: var(--r-pill);
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em;
  color: var(--fg-dim); text-transform: uppercase; font-weight: 600;
}
.chip.solid { background: var(--bg-2); }
.chip.accent { color: var(--accent); border-color: oklch(0.55 0.18 143 / 0.4); background: var(--accent-bg); }
.chip.info { color: var(--info); border-color: oklch(0.50 0.14 225 / 0.4); background: var(--info-bg); }
.chip.warn { color: var(--warn); border-color: oklch(0.55 0.14 80 / 0.4); background: var(--warn-bg); }
.chip.danger { color: var(--danger); border-color: oklch(0.55 0.18 20 / 0.4); background: var(--danger-bg); }
.chip.purple { color: var(--purple); border-color: oklch(0.55 0.16 300 / 0.4); background: var(--purple-bg); }

.diff-easy, .diff-facil { color: var(--accent); }
.diff-med, .diff-media { color: var(--warn); }
.diff-hard, .diff-dificil { color: var(--danger); }
.diff-insane { color: var(--purple); }

.pill {
  padding: 7px 13px; background: var(--bg-1);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  color: var(--fg-dim); font-family: var(--mono); font-size: 11.5px;
  cursor: pointer; transition: 0.16s var(--t);
  display: inline-flex; align-items: center; gap: 7px; text-decoration: none;
}
.pill:hover { color: var(--fg); border-color: var(--line-bright); text-decoration: none; }
.pill.active { color: var(--accent); background: var(--accent-bg); border-color: oklch(0.55 0.18 143 / 0.4); box-shadow: 0 0 16px -6px var(--accent-glow); }
.pill .count { color: var(--fg-mute); font-size: 10px; padding: 1px 6px; border-radius: var(--r-pill); background: var(--bg); }
.pill.active .count { color: var(--accent); background: oklch(0.55 0.18 143 / 0.2); }

/* ---------- Barra contextual ---------- */
.ctxbar { display: flex; align-items: center; gap: 6px; padding: 12px 0 0; flex: none; font-family: var(--mono); font-size: 11.5px; flex-wrap: wrap; }
.ctxbar-tab { padding: 6px 13px; border-radius: var(--r-pill); color: var(--fg-mute); cursor: pointer; transition: 0.14s var(--t); border: 1px solid transparent; text-decoration: none; }
.ctxbar-tab:hover { color: var(--fg); background: var(--bg-2); text-decoration: none; }
.ctxbar-tab.active { color: var(--accent); background: var(--accent-bg); border-color: oklch(0.55 0.18 143 / 0.35); }
.ctxbar-divider { width: 1px; height: 18px; background: var(--line); margin: 0 6px; }
.ctxbar-meta { color: var(--fg-mute); margin-left: auto; display: flex; gap: 14px; align-items: center; }
.ctxbar-meta b { color: var(--fg); font-weight: 500; }
.ctxbar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* ---------- Tarjetas ---------- */
.card, .card.bg-dark {
  background: var(--card-grad);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-1);
  margin-bottom: 18px;
}
.card::before { display: none; }
.card-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--line-soft); }
.card-title {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.22em;
  color: var(--fg-faint); text-transform: uppercase;
  display: flex; align-items: center; gap: 8px; font-weight: 600; margin: 0;
}
.card-title::before { content: ""; width: 6px; height: 6px; border-radius: 2px; background: var(--accent); box-shadow: 0 0 8px var(--accent); flex: none; }
.card-pad, .card-body { padding: 18px; }

.grid-2 { display: grid; grid-template-columns: 1.3fr 1fr; gap: 18px; }
.grid-2-eq { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }

/* ---------- Estadísticas ---------- */
.stat {
  padding: 18px; background: var(--card-grad);
  border: 1px solid var(--line-soft); border-radius: var(--r-md);
  position: relative; overflow: hidden; transition: 0.18s var(--t);
  box-shadow: var(--shadow-1);
}
.stat:hover { border-color: oklch(0.55 0.18 143 / 0.35); transform: translateY(-2px); box-shadow: var(--shadow-2), 0 0 30px -14px var(--accent-glow); }
.stat::after { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, oklch(0.90 0.23 140 / 0.5), transparent); }
.stat-label {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.2em;
  color: var(--fg-faint); text-transform: uppercase;
  display: flex; justify-content: space-between; align-items: center; font-weight: 600;
  margin: 0;
}
.stat-label .badge { color: var(--fg-mute); padding: 1px 6px; border: 1px solid var(--line-soft); border-radius: var(--r-pill); background: transparent; }
.stat-val {
  font-family: var(--mono); font-size: 30px; font-weight: 700;
  letter-spacing: -0.03em; margin: 10px 0 0; font-variant-numeric: tabular-nums;
  line-height: 1.1; color: var(--fg);
}
.stat-val .unit { font-size: 16px; color: var(--fg-mute); font-weight: 500; margin-left: 2px; }
.stat-delta { font-family: var(--mono); font-size: 11px; color: var(--accent); margin-top: 5px; display: flex; gap: 6px; align-items: center; }
.stat-delta.down { color: var(--danger); }
.stat-delta.flat { color: var(--fg-mute); }
.stat-spark { margin-top: 10px; height: 28px; }

/* ---------- Tablas ---------- */
.data-table, .table { font-size: 13px; width: 100%; border-collapse: collapse; }
.data-table thead th, .table thead th {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.16em;
  color: var(--fg-faint); text-transform: uppercase; text-align: left;
  padding: 12px 16px; border-bottom: 1px solid var(--line); border-top: 0;
  background: oklch(0.18 0.014 260 / 0.6); user-select: none; font-weight: 600;
}
.data-table thead th.right, .data-table tbody td.right { text-align: right; }
.data-table thead th.center, .data-table tbody td.center { text-align: center; }
.data-table tbody td, .table tbody td { padding: 12px 16px; border: 0; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
.data-table tbody tr, .table tbody tr { transition: 0.14s var(--t); }
.data-table tbody tr:hover, .table tbody tr:hover { background: oklch(0.26 0.018 260 / 0.5); }
.data-table tbody tr.you { background: var(--accent-bg); box-shadow: inset 2px 0 0 var(--accent); }
.data-table .num, .table .num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.data-table .row-link { cursor: pointer; }

.rank-cell { font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 0.02em; color: var(--fg-mute); }
.rank-medal { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: var(--r-sm); background: var(--bg-2); border: 1px solid var(--line); font-family: var(--mono); font-size: 10px; font-weight: 700; }
.rank-medal.r1 { background: var(--warn-bg); border-color: oklch(0.55 0.14 80 / 0.5); color: var(--warn); box-shadow: 0 0 14px -4px oklch(0.85 0.17 80 / 0.6); }
.rank-medal.r2 { background: oklch(0.42 0.008 260 / 0.5); border-color: oklch(0.58 0.01 260); color: var(--fg); }
.rank-medal.r3 { background: oklch(0.32 0.07 50 / 0.35); border-color: oklch(0.55 0.11 50 / 0.5); color: oklch(0.76 0.13 50); }

/* ---------- Máquinas ---------- */
.active-machine-banner {
  display: grid; grid-template-columns: 90px 1fr auto; gap: 18px;
  padding: 20px 22px;
  background: linear-gradient(110deg, var(--accent-bg), oklch(0.20 0.016 260) 55%);
  border: 1px solid oklch(0.55 0.18 143 / 0.35); border-radius: var(--r-lg);
  margin-bottom: 20px; align-items: center; position: relative; overflow: hidden;
  box-shadow: 0 0 40px -18px var(--accent-glow);
}
.active-machine-banner::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--accent); box-shadow: 0 0 16px var(--accent); }
.machine-glyph { width: 72px; height: 72px; border-radius: var(--r-md); background: var(--bg); border: 1px solid var(--line); display: grid; place-items: center; font-size: 34px; overflow: hidden; }
.machine-glyph img { width: 100%; height: 100%; object-fit: contain; padding: 10px; }
.machine-glyph.win { background: var(--info-bg); border-color: oklch(0.50 0.14 225 / 0.4); color: var(--info); box-shadow: 0 0 24px -8px oklch(0.80 0.15 225 / 0.6); }
.machine-glyph.lin { background: var(--warn-bg); border-color: oklch(0.55 0.14 80 / 0.4); color: var(--warn); box-shadow: 0 0 24px -8px oklch(0.85 0.17 80 / 0.5); }
.active-machine-meta { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.active-machine-row1 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.active-machine-name { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; }
.active-machine-row2 { display: flex; gap: 20px; font-family: var(--mono); font-size: 11.5px; color: var(--fg-mute); margin-top: 8px; flex-wrap: wrap; }
.active-machine-row2 b { color: var(--fg); font-weight: 500; }
.active-machine-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

.machine-card {
  background: var(--card-grad); border: 1px solid var(--line-soft);
  border-radius: var(--r-md); overflow: hidden; transition: 0.2s var(--t);
  box-shadow: var(--shadow-1); display: flex; flex-direction: column;
  text-decoration: none; color: inherit;
}
.machine-card:hover { border-color: oklch(0.55 0.18 143 / 0.4); transform: translateY(-4px); box-shadow: var(--shadow-2), 0 0 40px -16px var(--accent-glow); text-decoration: none; color: inherit; }
.machine-card-art {
  aspect-ratio: 16/9; background: var(--bg);
  display: grid; place-items: center; font-size: 64px;
  border-bottom: 1px solid var(--line-soft); position: relative; overflow: hidden;
}
.machine-card-art::before {
  content: ""; position: absolute; inset: 0;
  background-image: linear-gradient(var(--line-soft) 1px, transparent 1px), linear-gradient(90deg, var(--line-soft) 1px, transparent 1px);
  background-size: 20px 20px; opacity: 0.35;
  mask-image: radial-gradient(ellipse at center, black 20%, transparent 72%);
  -webkit-mask-image: radial-gradient(ellipse at center, black 20%, transparent 72%);
}
.machine-card-art.win { background: radial-gradient(circle at 50% 40%, oklch(0.30 0.08 225), var(--bg-1)); color: var(--info); }
.machine-card-art.lin { background: radial-gradient(circle at 50% 40%, oklch(0.30 0.07 80), var(--bg-1)); color: var(--warn); }
.machine-card-art .glyph { position: relative; z-index: 1; filter: drop-shadow(0 4px 20px oklch(0 0 0 / 0.5)); transition: 0.25s var(--t); max-height: 72%; max-width: 72%; }
.machine-card:hover .machine-card-art .glyph { transform: scale(1.08); }
.machine-card-status {
  position: absolute; top: 12px; right: 12px; padding: 4px 10px;
  background: oklch(0.12 0.01 260 / 0.7); backdrop-filter: blur(6px);
  border: 1px solid var(--line); border-radius: var(--r-pill);
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.16em;
  color: var(--fg-dim); display: flex; align-items: center; gap: 6px; z-index: 2;
}
.machine-card-status.live { color: var(--accent); border-color: oklch(0.55 0.18 143 / 0.5); }
.machine-card-status .pulse { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 6px var(--accent); animation: cc-pulse 1.6s infinite; }
.machine-card-body { padding: 16px; flex: 1; display: flex; flex-direction: column; }
.machine-card-title { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; }
.machine-card-meta { font-family: var(--mono); font-size: 11px; color: var(--fg-mute); margin-top: 5px; }
.machine-card-stats { display: flex; gap: 14px; margin-top: auto; padding-top: 12px; border-top: 1px solid var(--line-soft); font-family: var(--mono); font-size: 11px; color: var(--fg-dim); align-items: center; }
.machine-card-stats span { display: inline-flex; align-items: center; gap: 5px; }
.machine-card-stats span b { color: var(--fg); font-weight: 600; }

/* ---------- Topología ---------- */
.topology { background: var(--bg); border: 1px solid var(--line-soft); border-radius: var(--r-md); padding: 20px; position: relative; }
.topology svg { width: 100%; height: 220px; display: block; }
.topology-node { fill: var(--bg-2); stroke: var(--line-bright); stroke-width: 1; }
.topology-node.you { fill: var(--accent-bg); stroke: var(--accent-dim); }
.topology-node.live { fill: oklch(0.55 0.18 143 / 0.20); stroke: var(--accent-dim); }
.topology-edge { stroke: var(--line-bright); stroke-width: 1; stroke-dasharray: 3 3; }
.topology-edge.live { stroke: var(--accent); animation: cc-dash 1.5s linear infinite; stroke-dasharray: 4 4; }
.topology-label { font-family: var(--mono); font-size: 10px; fill: var(--fg-dim); }
.topology-label.you { fill: var(--accent); }

/* ---------- Retos ---------- */
.ch-row-name { display: flex; align-items: center; gap: 12px; }
.ch-row-cat { width: 4px; height: 32px; border-radius: var(--r-pill); flex: none; background: var(--fg-mute); }
.ch-row-cat.web { background: var(--info); box-shadow: 0 0 10px -2px var(--info); }
.ch-row-cat.crypto { background: var(--purple); box-shadow: 0 0 10px -2px var(--purple); }
.ch-row-cat.pwn { background: var(--danger); box-shadow: 0 0 10px -2px var(--danger); }
.ch-row-cat.rev { background: var(--warn); box-shadow: 0 0 10px -2px var(--warn); }
.ch-row-cat.forensics { background: var(--pink); box-shadow: 0 0 10px -2px var(--pink); }
.ch-row-cat.osint { background: var(--accent); box-shadow: 0 0 10px -2px var(--accent); }
.ch-row-info { line-height: 1.3; }
.ch-row-title { font-size: 13.5px; font-weight: 600; color: var(--fg); }
.ch-row-title.solved { color: var(--accent); }
.ch-row-tags { display: flex; gap: 5px; margin-top: 5px; flex-wrap: wrap; }
.ch-row-tag { font-family: var(--mono); font-size: 9.5px; padding: 2px 7px; background: var(--bg-2); border-radius: var(--r-pill); color: var(--fg-mute); border: 1px solid var(--line-soft); }

/* ---------- Formularios ---------- */
.form-row { margin-bottom: 16px; }
.form-label, .form-group > label, .bmd-form-group > label {
  display: block; position: static;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em;
  color: var(--fg-mute); text-transform: uppercase; margin: 0 0 7px;
  transform: none; top: auto; left: auto;
}
.form-input, .form-control, input[type="text"], input[type="email"],
input[type="password"], input[type="search"], textarea, select {
  width: 100%; padding: 11px 14px;
  background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--fg); font-size: 13px; transition: 0.16s var(--t);
  min-height: 42px; box-shadow: none; background-image: none;
}
.form-input:focus, .form-control:focus, input:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--accent-dim); box-shadow: 0 0 0 3px var(--accent-bg); background: var(--bg-1);
}
.form-input.mono { font-family: var(--mono); }
.form-help { display: flex; justify-content: space-between; margin-top: 7px; font-family: var(--mono); font-size: 11px; color: var(--fg-mute); }
.form-help a { color: var(--accent); text-decoration: none; }
.check { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--fg-dim); cursor: pointer; }
.check input { accent-color: var(--accent); width: 16px; height: 16px; min-height: 0; }

.form-divider { display: flex; align-items: center; gap: 12px; margin: 20px 0; font-family: var(--mono); font-size: 10px; color: var(--fg-mute); letter-spacing: 0.2em; }
.form-divider::before, .form-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.form-foot { margin-top: 24px; font-size: 12.5px; color: var(--fg-mute); text-align: center; }
.form-foot a { color: var(--accent); text-decoration: none; }

/* ---------- Autenticación ---------- */
.auth-shell { min-height: 100vh; display: grid; grid-template-columns: 1fr 500px; position: relative; z-index: 1; }
.auth-art {
  background: linear-gradient(160deg, oklch(0.20 0.016 262), oklch(0.16 0.012 260));
  border-right: 1px solid var(--line-soft);
  padding: 44px 52px; display: flex; flex-direction: column; gap: 32px;
  position: relative; overflow: hidden;
}
.auth-art::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(600px 500px at 20% 30%, oklch(0.55 0.18 143 / 0.12), transparent 60%),
              radial-gradient(500px 400px at 90% 90%, oklch(0.55 0.16 265 / 0.14), transparent 55%);
}
.auth-art::after {
  content: ""; position: absolute; inset: 0;
  background-image: linear-gradient(var(--line-soft) 1px, transparent 1px), linear-gradient(90deg, var(--line-soft) 1px, transparent 1px);
  background-size: 34px 34px; opacity: 0.25;
  mask-image: radial-gradient(ellipse at 30% 50%, black 0%, transparent 72%);
  -webkit-mask-image: radial-gradient(ellipse at 30% 50%, black 0%, transparent 72%);
}
.auth-art > * { position: relative; z-index: 1; }
.auth-headline { font-size: 58px; font-weight: 800; letter-spacing: -0.04em; line-height: 0.98; margin: auto 0 0; }
.auth-headline .accent { color: var(--accent); text-shadow: 0 0 40px var(--accent-glow); }
.auth-blurb { color: var(--fg-dim); font-size: 14px; line-height: 1.6; max-width: 440px; }

.auth-form-wrap { padding: 44px 52px; display: flex; flex-direction: column; justify-content: center; background: var(--bg); }
.auth-form { max-width: 380px; width: 100%; margin: 0 auto; }
.form-h { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; margin: 0; }
.form-sub { color: var(--fg-mute); font-size: 13px; margin: 6px 0 26px; }

.auth-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.auth-stat strong { display: block; font-family: var(--mono); font-size: 26px; font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.auth-stat span { font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em; color: var(--fg-mute); text-transform: uppercase; }

/* ---------- Progreso, esqueleto, aviso ---------- */
/* El CARRIL mide 7px; el RELLENO no impone altura propia.
 *
 * Antes esta regla ponia `height: 7px` tambien a `.progress-bar`, y en Bootstrap
 * .progress-bar es el relleno, no el carril — y ademas es donde el marcado de
 * este proyecto escribe la etiqueta del porcentaje. Resultado medido en
 * /target/1 y /target/2: texto de 13px dentro de una caja de 7px con
 * overflow:hidden, o sea la mitad superior de las letras. Son 8 sitios, no uno.
 *
 * Es la colision de nombres tipica al importar un sistema de diseno sobre un
 * marcado que ya existia: la misma clase significaba "adorno" en el diseno y
 * "contenedor con etiqueta" en las vistas. Se separa: el carril fija la altura,
 * el relleno la hereda, y si lleva texto la caja crece para que quepa. */
.progress { height: 7px; background: var(--bg-2); border-radius: var(--r-pill); overflow: hidden; position: relative; box-shadow: none; }
.progress-bar { background: var(--bg-2); border-radius: var(--r-pill); box-shadow: none; }

/* Barra con etiqueta dentro: manda el texto. Se detecta por el atributo que ya
   escriben las vistas, sin tener que anadir una clase a cada una. */
.progress:has(> .progress-bar:not(:empty)) { height: auto; min-height: 7px; }
.progress > .progress-bar:not(:empty) {
  height: auto; min-height: 18px; overflow: visible;
  display: flex; align-items: center; justify-content: center;
  padding: 1px 8px; font-size: 11px; line-height: 1.4; font-weight: 600;
}
.progress-fill, .progress .progress-bar {
  height: 100%; background: linear-gradient(90deg, var(--accent-dim), var(--accent));
  border-radius: var(--r-pill); transition: width 0.4s var(--t);
  box-shadow: 0 0 12px -2px var(--accent-glow);
}
.skeleton { background: linear-gradient(90deg, var(--bg-2), var(--bg-3), var(--bg-2)); background-size: 400px 100%; animation: cc-shimmer 1.5s infinite linear; border-radius: var(--r-sm); }

.toast {
  position: fixed; bottom: 42px; right: 26px; padding: 12px 16px;
  background: var(--card-grad); border: 1px solid var(--line-bright);
  border-radius: var(--r-md); font-size: 13px;
  display: flex; gap: 10px; align-items: center; z-index: 200;
  box-shadow: var(--shadow-2); animation: cc-toastIn 0.25s var(--t);
}
.toast.ok { border-color: var(--accent-dim); box-shadow: var(--shadow-2), 0 0 30px -12px var(--accent-glow); }
.toast.err { border-color: oklch(0.55 0.18 20 / 0.5); }

/* ---------- Estado vacío ---------- */
.cc-empty {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 48px 24px; text-align: center;
  border: 1px dashed var(--line); border-radius: var(--r-md);
  color: var(--fg-mute);
}
.cc-empty .glyph { width: 46px; height: 46px; display: grid; place-items: center; border-radius: var(--r-md); background: var(--bg-2); border: 1px solid var(--line-soft); color: var(--fg-faint); }
.cc-empty h2 { font-size: 15px; color: var(--fg); margin: 0; font-weight: 600; }
.cc-empty p { margin: 0; max-width: 46ch; font-size: 12.5px; }

/* ============================================================
   ANIMACIONES
   ============================================================ */
@keyframes cc-dash { to { stroke-dashoffset: -16; } }
@keyframes cc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
@keyframes cc-shimmer { 0% { background-position: -400px 0; } 100% { background-position: 400px 0; } }
@keyframes cc-fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes cc-toastIn { from { transform: translateY(8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

.content > .container,
.content > .container-fluid { animation: cc-fadeUp 0.3s var(--t); }

/* WCAG 2.1 SC 2.3.3: quien pide menos movimiento no recibe ninguno. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .machine-card:hover, .stat:hover, .btn:hover { transform: none; }
}

/* Foco visible en todo lo interactivo (WCAG 2.4.7). */
a:focus-visible, button:focus-visible, .btn:focus-visible,
.nav-item:focus-visible, .icon-btn:focus-visible, .pill:focus-visible,
.ctxbar-tab:focus-visible, input:focus-visible, select:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width: 1100px) {
  .grid-3 { grid-template-columns: 1fr 1fr; }
  .grid-4 { grid-template-columns: 1fr 1fr; }
  .grid-2 { grid-template-columns: 1fr; }
  .search { min-width: 200px; }
}

@media (max-width: 980px) {
  .auth-shell { grid-template-columns: 1fr; }
  .auth-art { display: none; }
  .auth-form-wrap { padding: 32px 24px; }
}

@media (max-width: 760px) {
  .cc-app { grid-template-columns: 1fr; grid-template-areas: "main" "status"; }
  /* La barra lateral pasa a panel deslizante; el tema heredado ya alterna
   * .nav-open sobre <html> desde su propio JS. */
  .cc-app > .sidebar {
    position: fixed !important;
    top: 0; bottom: 0; left: 0; width: 256px !important;
    transform: translateX(-100%) !important;
    /* Sin transicion sobre transform, a proposito.
     *
     * Medido: al abrir el menu quedaba una CSSTransition en estado "running"
     * con currentTime clavado en 0, y como en la cascada las transiciones van
     * POR ENCIMA incluso de las declaraciones !important, la barra se quedaba
     * congelada en su valor inicial —fuera de pantalla— pasara lo que pasara.
     * getComputedStyle ya devolvia translateX(0) mientras la caja seguia en
     * -256px: el estilo correcto aplicado y el pixel sin moverse.
     *
     * Un deslizamiento de 200ms no vale lo que cuesta: sin el, el panel aparece
     * y desaparece al instante y la navegacion movil funciona siempre. */
    z-index: 40;
  }
  html.nav-open .cc-app > .sidebar { transform: translateX(0) !important; }
  .grid-3, .grid-4, .grid-2-eq { grid-template-columns: 1fr; }
  .content { padding: 18px 14px 32px !important; }
  .page-head { grid-template-columns: 1fr; }
  .page-title { font-size: 24px; }
  .search { display: none; }
  .active-machine-banner { grid-template-columns: 1fr; }
  .statusbar { overflow-x: auto; }
}

/* ============================================================
   CORRECCIONES MEDIDAS SOBRE LA PAGINA SERVIDA
   ============================================================ */

/* --- La marca se leia en una sola linea ---------------------------------
 * En la maqueta React estas dos piezas eran <div>; aqui son <span> dentro de
 * otro <span>, asi que fluian en linea y "ACADEMIA CYBER RANGE" y "CONSOLA DE
 * OPERADOR" quedaban pegadas.
 */
.brand-text { display: flex; flex-direction: column; }
.brand-text-1, .brand-text-2 { display: block; }
.brand-text-1 { line-height: 1.15; }

/* --- El punto de acento salia delante de cada cifra ----------------------
 * `.card-title` en el diseno es la ETIQUETA de un panel, y por eso lleva un
 * cuadrito de acento delante. En las tarjetas de estadistica de esta
 * plataforma `.card-title` es el VALOR (`.card-category` es la etiqueta), asi
 * que el punto aparecia pegado al numero: "· 1", "· 15".
 */
.card.card-stats .card-title::before,
.stat .card-title::before { content: none; }

.card.card-stats > .card-header > .card-title,
.card.card-stats > .card-header > h4.card-title {
  font-family: var(--mono);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: -0.03em;
  text-transform: none;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.card.card-stats > .card-header > .card-category {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--fg-faint);
  font-weight: 600;
}

/* El icono de la tarjeta de estadistica, con la forma del diseno. */
.card.card-stats .card-icon {
  width: 44px; height: 44px;
  background: var(--accent-bg) !important;
  border: 1px solid oklch(0.55 0.18 143 / 0.3);
  border-radius: var(--r-sm);
  color: var(--accent);
  box-shadow: none !important;
}
.card.card-stats .card-icon svg,
.card.card-stats .card-icon i { color: var(--accent); font-size: 1.15rem !important; }

/* Filo superior de acento, como .stat en el diseno. */
.card.card-stats { position: relative; overflow: hidden; }
.card.card-stats::after {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: linear-gradient(90deg, transparent, oklch(0.90 0.23 140 / 0.5), transparent);
}
.card.card-stats:hover { border-color: oklch(0.55 0.18 143 / 0.35); transform: translateY(-2px); box-shadow: var(--shadow-2), 0 0 30px -14px var(--accent-glow); }

/* --- Encabezado de tarjeta con icono ------------------------------------
 * El marcado del tema heredado mete un <i> junto al texto del titulo; con el
 * cuadrito del diseno delante quedaban dos marcas seguidas.
 */
.card-title i, .card-title .material-icons, .card-title svg { display: none; }
.card-head .card-title i { display: inline-flex; }

/* --- Estados vacios dentro de tarjetas ----------------------------------- */
.card-body > .empty,
.card .list-group:empty {
  display: block; padding: 40px 16px; text-align: center;
  color: var(--fg-mute); font-size: 12.5px;
}

/* ── Miniaturas (app\components\Thumbnail) ─────────────────────────────── */
/* Un monograma SVG cuando la maquina no tiene imagen generada. Alineado con
   el texto que lo acompana y sin poder seleccionarse: es un adorno, no texto. */
.ac-thumb {
  flex: 0 0 auto;
  vertical-align: middle;
  border-radius: 6px;
  user-select: none;
}
.ac-thumb-mono { display: inline-block; }
img.ac-thumb {
  object-fit: cover;
  background: var(--surface-2, var(--bg));
  border: 1px solid var(--border);
}

/* ── Cabeceras de tarjeta heredadas ────────────────────────────────────── */
/* material-dashboard flota la cabecera fuera de la tarjeta con margenes
   negativos y la pinta con degradados de color (danger, primary, success...).
   En una consola oscura eso da bloques rojos y naranjas sin significado, y el
   margen negativo recortaba el titulo contra el borde superior. Se neutraliza
   la flotacion y el color; el significado lo lleva el texto, no el fondo. */
.card .card-header,
.card .card-header[class*="card-header-"] {
  margin: 0;
  padding: 14px 18px;
  background: transparent !important;
  background-image: none !important;
  box-shadow: none !important;
  border-radius: 0;
  border-bottom: 1px solid var(--border);
}
.card .card-header .card-title {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 650;
  letter-spacing: 0.01em;
  /* `var(--text)` NO EXISTE. No está definida en ninguna de las siete hojas que
   * se cargan, así que esta declaración es inválida y el navegador la descarta:
   * el color acaba heredándose. Comprobado. La variable de este sistema es
   * `--fg`. */
  color: var(--fg);
}
.card .card-header .card-category {
  margin: 2px 0 0;
  font-size: 0.78rem;
  /* Igual que arriba: `var(--muted)` tampoco existe. Con las dos inválidas, el
   * título de la tarjeta y su descripción salían EXACTAMENTE del mismo blanco,
   * así que no había ninguna jerarquía entre lo que algo se llama y lo que
   * explica. Contraste sobraba —16,5:1—; lo que faltaba era diferencia. */
  color: var(--fg-mute);
}
/* La variante -icon coloca un cuadrado de color con margen negativo. Se deja
   como chip alineado en linea, sin sacarlo de la caja. */
.card .card-header-icon .card-icon {
  float: none;
  margin: 0 10px 0 0;
  padding: 6px;
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 7px;
  background: var(--accent-bg) !important;
  color: var(--accent) !important;
  box-shadow: none !important;
}

/* ── Etiquetas de columna con icono ────────────────────────────────────── */
.th-ico { display: inline-flex; align-items: center; justify-content: center; }
/* sr-only: bootstrap4 ya la define, pero la tabla se usa tambien fuera de el. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ── Barra de avance ───────────────────────────────────────────────────── */
.progress-avance {
  height: 10px;
  background: var(--surface-2, oklch(0.235 0.017 260));
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.progress-avance .progress-bar,
.progress-avance .bg-gradual-progress {
  background: linear-gradient(90deg, var(--accent-2, var(--accent)), var(--accent));
  border-radius: 999px;
}

/* ── Pie de las tarjetas de estadistica ────────────────────────────────── */
/* material-dashboard pinta .stats con #333333 sobre un tema que asume claro.
   Sobre el fondo de la consola eso da 1.55:1 — texto practicamente invisible,
   y es donde va el dato que compara tu avance con el total ("4 entregadas por
   ti"). Medido tras el cambio: pasa AA. */
.card .stats,
.card-footer .stats,
.stats {
  color: var(--fg-mute) !important;
}
.card .stats .material-icons,
.card .stats i {
  color: var(--fg-faint) !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   PUENTE DE TOKENS: la paleta anterior pasa a ser la nueva
   ══════════════════════════════════════════════════════════════════════════
   academia.css declara 21 tokens --ac-* con valores hexadecimales propios y
   los usa a lo largo de ~1900 lineas. Al importar CyberConquer quedaron dos
   paletas vivas a la vez, y se notaba: el acento antiguo (#94c11f, un verde
   apagado) convivia con el nuevo (lima brillante) segun que componente
   pintara cada cosa, y el gris tenue #6b7285 daba 3.48:1 — por debajo de AA.

   Se podria haber reescrito academia.css entero. Reapuntar los tokens hace lo
   mismo con 21 lineas y sin tocar ni una regla: todo lo que ya usaba la
   paleta antigua queda automaticamente dentro del sistema nuevo, y cuando se
   ajuste un color del diseno se movera tambien lo heredado.

   Va al final del archivo a proposito: cyberconquer.css se emite despues de
   academia.css (AcademiaAsset lista los dos en ese orden), asi que estas
   declaraciones ganan a las originales sin necesidad de !important.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  /* superficies */
  --ac-fondo: var(--bg);
  --ac-fondo-elevado: var(--bg-1);
  --ac-fondo-hundido: var(--bg-2);
  /* texto */
  --ac-texto: var(--fg);
  --ac-texto-suave: var(--fg-dim);
  --ac-texto-tenue: var(--fg-mute);
  /* lineas */
  --ac-borde: var(--line);
  --ac-borde-suave: var(--line-soft);
  /* color */
  --ac-lima: var(--accent);
  --ac-lima-oscuro: var(--accent-dim);
  --ac-cian: var(--info);
  --ac-azul: var(--info);
  --ac-amarillo: var(--warn);
  --ac-rojo: var(--danger);
  --ac-magenta: var(--purple);
  /* tipografia */
  --ac-fuente: var(--sans);
  --ac-fuente-cifras: var(--mono);
  /* forma */
  --ac-radio: var(--r-md);
  --ac-radio-chico: var(--r-sm);
  --ac-espacio: 4px;
  --ac-sombra: var(--sombra-md, 0 8px 24px oklch(0 0 0 / 0.45));
}

/* ── Movil: correcciones medidas a 390px ───────────────────────────────── */
@media (max-width: 760px) {
  /* La barra de estado tiene cuatro celdas pensadas para 1440px. A 390px las
     dos ultimas se salian a 518px y 602px, fuera de la pantalla. En vez de
     dejar que se corten, se ocultan las secundarias: el estado de la VPN es lo
     unico que hay que poder ver siempre, el resto ya esta en la barra lateral. */
  .statusbar { overflow: hidden; }
  .statusbar .status-cell:nth-child(n+3) { display: none; }
  .statusbar .status-cell { min-width: 0; }
  .statusbar .status-cell .v { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Objetivos tactiles: WCAG 2.1 SC 2.5.8 pide 24x24 CSS px. Medidos 13 enlaces
     de 14-17px de alto (la descarga del perfil VPN, el usuario, las migas).
     Se les da altura de pulsacion sin cambiar como se ven, con area extendida
     mediante padding vertical y un alto minimo. */
  .side-foot a,
  .user-card,
  .breadcrumb a,
  .statusbar a {
    min-height: 24px;
    display: inline-flex;
    align-items: center;
    padding-block: 4px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   ENTREGA DE BANDERAS · barra superior
   ══════════════════════════════════════════════════════════════════════════
   Es el control mas importante de la aplicacion y no existia. Vive en la barra
   superior porque una bandera aparece mientras trabajas en cualquier pantalla,
   no solo en la ficha de la maquina. */
.claim { display: flex; align-items: center; }
.claim .navbar-form { margin: 0; }
.claim .input-group {
  /* nowrap explicito: .input-group de Bootstrap trae `flex-wrap: wrap`, y como
     el .form-control de material.css se lleva el 100% del ancho, el boton caia
     a una segunda linea. Medido: el grupo ocupaba 80px de alto dentro de una
     barra de 60px, se salia por arriba (y=-10) y tapaba las migas. */
  display: flex; flex-wrap: nowrap; align-items: center; gap: 0;
  background: var(--bg-1);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  transition: border-color 0.16s var(--t), box-shadow 0.16s var(--t);
}
.claim .input-group:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg);
}
.claim .form-control {
  /* Se fija la geometria completa: material.css impone su propia altura y un
     width:100% que aqui rompen la barra. */
  flex: 0 1 210px; width: 210px; max-width: 210px;
  height: 34px; min-height: 34px; line-height: 34px;
  padding: 0 10px; margin: 0;
  background: transparent !important;
  border: 0 !important; box-shadow: none !important;
  color: var(--fg); font-family: var(--mono); font-size: 12px;
}
.claim .form-control::placeholder { color: var(--fg-mute); font-family: var(--sans); }
.claim .btn, .claim .btn-just-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; min-height: 34px; padding: 0; margin: 0;
  background: var(--accent-bg); border: 0; border-radius: 0 var(--r-sm) var(--r-sm) 0;
  color: var(--accent); box-shadow: none;
}
.claim .btn:hover { background: var(--accent); color: #06210a; }
.claim .btn .material-icons { font-size: 17px; }

/* ── Resultados del buscador ─────────────────────────────────────────────── */
.search { position: relative; }
.search-results {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  margin: 0; padding: 4px; list-style: none; z-index: 1200;
  background: var(--bg-1); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: 0 18px 40px -12px oklch(0 0 0 / 0.6);
  max-height: 380px; overflow-y: auto;
}
.search-results[hidden] { display: none; }
.search-results li { margin: 0; }
.search-results a {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 9px; border-radius: var(--r-sm);
  color: var(--fg-dim); text-decoration: none; font-size: 12.5px;
  min-height: 34px;
}
.search-results a:hover,
.search-results li.activa a { background: var(--bg-2); color: var(--fg); text-decoration: none; }
.search-results .search-thumb { display: inline-flex; flex: 0 0 auto; }
.search-results .search-name { flex: 1; font-family: var(--mono); }
.search-results .search-dif { color: var(--fg-mute); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em; }
.search-empty { padding: 10px; color: var(--fg-mute); font-size: 12px; text-align: center; }

@media (max-width: 1100px) {
  /* En pantallas medias cabe la entrega de banderas o el buscador, no ambos.
     Se conserva la entrega: es la accion, no la consulta. */
  .search { display: none; }
}
@media (max-width: 760px) {
  .claim .form-control { width: 130px; }
}

/* Aviso de listado filtrado. Sin el, una tabla mas corta se lee como "faltan
   maquinas" en vez de como "hay un filtro puesto". */
.filtro-activo {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 0 12px; padding: 8px 12px;
  background: var(--accent-bg); border: 1px solid oklch(0.55 0.18 143 / 0.35);
  border-radius: var(--r-sm); color: var(--fg-dim); font-size: 12.5px;
}
.filtro-activo strong { color: var(--accent); font-family: var(--mono); }
.filtro-activo .quitar-filtro { margin-left: auto; color: var(--fg-mute); min-height: 24px; display: inline-flex; align-items: center; }
.filtro-activo .quitar-filtro:hover { color: var(--fg); }

/* Cifras de la tarjeta de maquina: una por linea, alineadas por su icono.
   Antes era un solo <p> con <br/> y el texto quedaba desalineado. */
.target-cifras { display: flex; flex-direction: column; gap: 4px; margin: 8px 0 0; }
.target-cifras .cifra {
  display: flex; align-items: baseline; gap: 7px;
  color: var(--fg-dim); font-size: 12.5px;
}
.target-cifras .cifra i { color: var(--accent); font-size: 11px; width: 13px; text-align: center; flex: 0 0 auto; }
.target-cifras .cifra-detalle { color: var(--fg-mute); font-size: 11px; }


/* La barra superior no deja que ningun control la desborde en alto.
   Antes el grupo de entrega de banderas se salia por arriba y tapaba las migas. */
.topbar > * { max-height: 44px; }
.topbar .claim, .topbar .claim .input-group { max-height: 36px; }

/* ══════════════════════════════════════════════════════════════════════════
   CABECERA DE LA FICHA DE MAQUINA
   ══════════════════════════════════════════════════════════════════════════
   material.css trae esta regla:

     .target-card > div > div.card-header.card-header-icon > h4.card-title {
       background: #94c11f; color: black; }

   Es la lima ANTIGUA del tema, codificada como literal (no como variable), con
   una especificidad de (0,4,2) que ninguna hoja del diseno alcanza escribiendo
   selectores razonables. Resultado medido en /target/1 y /target/2: un bloque
   verde chillon con el nombre de la maquina y su IP en negro, a 30px, que
   ademas se salia 64px del borde de la tarjeta y quedaba cortado ("10.10.10.1").

   Aqui se usa !important a proposito, y es la excepcion, no la norma: la regla
   de origen esta en una hoja vendida que no conviene editar —se perderia en la
   proxima actualizacion del tema— y su especificidad es artificialmente alta
   por la cadena de descendientes. Subir la nuestra a (0,5,2) para empatar seria
   escalar la misma guerra que causo el problema.

   Se cierra la CLASE, no solo este caso: cualquier .card-title dentro de
   cualquier variante card-header-*. */
.card .card-header[class*="card-header-"] .card-title,
.target-card > div > div.card-header.card-header-icon > .card-title {
  background: transparent !important;
  background-image: none !important;
  color: var(--fg) !important;
  /* El titulo lleva nombre + IP, longitud variable. En una rejilla, una pista
     `1fr` no encoge por debajo de su contenido salvo con min-width:0. */
  min-width: 0;
  max-width: 100%;
  font-size: 17px;
  font-weight: 650;
  line-height: 1.3;
  padding: 0;
  overflow: hidden;
  flex-wrap: wrap;
  gap: 2px 8px;
}
.card .card-header[class*="card-header-"] .card-title > * {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* La IP, en monoespaciada y en su propio renglon: es un dato que se copia. */
.card .card-header .card-title span[id^="target_id_"] {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--accent);
  font-weight: 500;
}
/* La categoria (dificultad, etiquetas) tampoco puede desbordar.
 *
 * ACOTADA A LAS TARJETAS DE MÁQUINA, que es para quien se escribió: ahí
 * `card-category` son dos o tres palabras sueltas y recortarlas con puntos
 * suspensivos es lo correcto.
 *
 * Sin acotar, alcanzaba también a las once tarjetas de Marcadores, donde ese
 * mismo hueco lleva una FRASE ENTERA. Medido a 1280 px:
 * «Quiénes han documentado más resoluciones.» ocupaba 254 px en 251
 * disponibles, así que se leía «…más re…» y el resto se perdía. Sin `title` ni
 * nada que lo revelara: pérdida de contenido en el ancho por omisión.
 */
.card.card-stats .card-header .card-category,
.target-card .card-header .card-category {
  min-width: 0; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ══════════════════════════════════════════════════════════════════════════
   UTILIDADES DE COLOR DE BOOTSTRAP PENSADAS PARA TEMA CLARO
   ══════════════════════════════════════════════════════════════════════════
   `.text-dark` significa "texto oscuro porque el fondo es claro". En esta
   aplicacion NO hay ninguna superficie clara: todo el rango es una consola
   oscura. Cada uso de esa clase pinta texto casi negro sobre fondo casi negro.

   Medidas tomadas:
     - boton "Ir a la red" en /networks: 1.17:1 (AA exige 4.5)
     - la IP de la maquina en /target/N: ilegible sobre la tarjeta

   Son 41 usos de `.text-dark` y 51 de `.bg-dark` repartidos por las vistas. No
   se corrigen uno a uno: se corrige lo que la clase significa en este tema, que
   es donde esta el error. Si algun dia hay una superficie clara de verdad,
   tendra su propia clase y estas seguiran siendo coherentes.

   `:not(.btn)` porque el color del texto de un boton lo decide su variante. */
/* `.progress-bar` queda fuera: esa clase se pinta SOBRE el degradado verde de
   acento, que es claro. Devolverle el color claro del tema deja texto claro
   sobre fondo claro — medido en las tres barras del Panel: 1,21:1 en el peor
   punto y 1,90:1 en el centro, con tipografía de 11 px. El minimo AA es 4,5:1.
   Sobre el acento hace falta tinta oscura, que es justo lo que `.text-dark`
   significa en su hoja original.
   No se quita `.text-dark` del selector: hay otros 40 usos sobre fondo oscuro
   —la IP de la maquina, el aviso de cuenta suspendida, los writeups— donde el
   color oscuro original si es ilegible. */
.text-dark:not(.btn):not(.progress-bar) { color: var(--fg) !important; }
.progress-bar.text-dark { color: var(--bg) !important; }
.text-light:not(.btn) { color: var(--fg-dim) !important; }
.text-muted:not(.btn) { color: var(--fg-mute) !important; }
.text-white:not(.btn) { color: var(--fg) !important; }
/* .bg-dark ya no aporta nada: el fondo de cada superficie lo da su componente.
   Dejarla activa pintaba un gris que no pertenece a la paleta. */
.bg-dark:not(.btn) { background-color: transparent !important; }

/* El campo de entrega de banderas no puede encogerse hasta ser inusable.
   Medido a 390px: el grupo quedaba en 58px de ancho total. Como a partir de
   1100px el buscador ya se oculta, aqui hay sitio de sobra: se le deja crecer. */
.topbar .claim { flex: 1 1 auto; min-width: 0; }
.topbar .claim .input-group { width: 100%; }
@media (max-width: 1100px) {
  .claim .form-control { flex: 1 1 auto; width: auto; max-width: none; min-width: 0; }
}
@media (max-width: 760px) {
  /* Las migas ceden el espacio: el nombre de la pantalla ya esta en el titulo. */
  .topbar .breadcrumbs { flex: 0 1 auto; min-width: 0; overflow: hidden; }
  .topbar .breadcrumbs a:not(:last-child), .topbar .breadcrumbs .sep { display: none; }
  .topbar .top-spacer { display: none; }
  .claim .form-control { min-width: 120px; }
}

/* Estado vacio de los desplegables: es texto, no una opcion pulsable. */
.dropdown-vacio {
  color: var(--fg-mute); font-size: 12px; cursor: default;
  padding: 10px 12px; display: block; text-align: center;
}
.dropdown-vacio:hover { background: transparent; color: var(--fg-mute); }

/* ══════════════════════════════════════════════════════════════════════════
   CABECERAS DE TARJETA: el margen negativo heredado
   ══════════════════════════════════════════════════════════════════════════
   material-dashboard.css trae esta regla:

     .card [class*="card-header-"]:not(.card-header-icon):not(.card-header-text)
       { margin: -20px; padding: 15px; }

   Especificidad (0,5,0) por la cadena de :not(). Es el patron de la "cabecera
   flotante" del tema original: saca la cabecera 20px fuera de la tarjeta para
   que el bloque de color sobresalga. Sin ese bloque de color —que el diseno ya
   retiro— lo unico que queda es un titulo pegado al borde superior, medido a
   15px del borde cuando el resto de la tarjeta respira 18px, y desalineado con
   el cuerpo.

   Se usa !important por el mismo motivo documentado en el bloque de
   .card-title: la regla de origen esta en una hoja vendida y su especificidad
   es artificialmente alta. Empatarla escribiendo (0,6,0) seria seguir la misma
   escalada que causo el problema.
   ══════════════════════════════════════════════════════════════════════════ */
.card > .card-header,
.card [class*="card-header-"] {
  margin: 0 !important;
  padding: 16px 18px 12px !important;
}
/* La variante -icon coloca ademas un cuadro flotante; ya neutralizado mas
   arriba, aqui solo se le quita el desplazamiento vertical. */
.card > .card-header.card-header-icon { padding-top: 16px !important; }

/* El cuerpo arranca alineado con la cabecera, no pegado a ella. */
.card > .card-body { padding: 14px 18px 18px; }
.card > .card-footer { padding: 12px 18px 16px; }

/* ══════════════════════════════════════════════════════════════════════════
   AVATAR DEL PERFIL
   ══════════════════════════════════════════════════════════════════════════
   Medido en /profile/settings: el archivo mide 255x255 y se pintaba a 265x265,
   o sea ESCALADO HACIA ARRIBA — mas grande que su resolucion, asi que ademas
   se ve borroso. Ocupaba media columna del formulario y empujaba todo lo demas
   fuera de la vista.

   Un avatar es un identificador, no una ilustracion: se acota a un tamano en el
   que se reconoce a la persona sin robarle sitio al contenido. */
.card .img-thumbnail,
.fileinput .thumbnail > img,
.fileinput-preview > img,
img.rounded.img-thumbnail {
  max-width: 160px;
  max-height: 160px;
  width: auto;
  height: auto;
  object-fit: cover;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  background: var(--bg-1);
  padding: 0;
}
/* El avatar de la ficha de perfil (columna derecha) tampoco puede desbordar. */
.profile-avatar img, .card .profile img.rounded { max-width: 140px; height: auto; }

/* ══════════════════════════════════════════════════════════════════════════
   ACCESO AL LABORATORIO (VPN) — bloque del panel
   ══════════════════════════════════════════════════════════════════════════
   Sin VPN no se llega a ninguna maquina, asi que este bloque es la primera
   accion real del alumno y se pinta como tal: destacado, con el estado a la
   vista y una sola accion evidente. Desaparece solo cuando el tunel esta
   levantado. */
.acceso-vpn {
  display: flex; align-items: center; gap: 24px; flex-wrap: wrap;
  margin: 0 0 22px; padding: 18px 22px;
  background: linear-gradient(120deg, var(--warn-bg), transparent 65%), var(--bg-1);
  border: 1px solid oklch(0.60 0.14 80 / 0.35);
  border-radius: var(--r-md);
}
.acceso-vpn--bloqueado {
  background: linear-gradient(120deg, var(--danger-bg), transparent 65%), var(--bg-1);
  border-color: oklch(0.55 0.18 20 / 0.35);
}
.acceso-vpn__texto { flex: 1 1 320px; min-width: 0; }
.acceso-vpn__eyebrow {
  display: flex; align-items: center; gap: 7px;
  margin: 0 0 6px; font-family: var(--mono); font-size: 10.5px;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--fg-mute);
}
.acceso-vpn__eyebrow .dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; }
.acceso-vpn__eyebrow .dot.warn { background: var(--warn); box-shadow: 0 0 8px var(--warn); }
.acceso-vpn__eyebrow .dot.err { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
.acceso-vpn h2 { margin: 0 0 6px; font-size: 17px; font-weight: 650; color: var(--fg); }
.acceso-vpn p { margin: 0; color: var(--fg-dim); font-size: 13px; line-height: 1.55; }
.acceso-vpn__acciones { display: flex; gap: 10px; flex-wrap: wrap; flex: 0 0 auto; }

@media (max-width: 760px) {
  .acceso-vpn { padding: 16px; gap: 14px; }
  .acceso-vpn__acciones { width: 100%; }
  .acceso-vpn__acciones .btn { flex: 1 1 auto; justify-content: center; }
}

/* Explicacion de por que una maquina no ofrece acciones. Es informacion, no
   una alerta: tono neutro y discreto. */
.target-actions-vacio {
  display: flex; align-items: flex-start; gap: 7px;
  margin: 8px 0 0; padding: 8px 10px;
  background: var(--bg-2); border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--fg-mute); font-size: 12px; line-height: 1.45;
}
.target-actions-vacio i { color: var(--warn); margin-top: 2px; flex: 0 0 auto; }

/* ══════════════════════════════════════════════════════════════════════════
   PIE DE TARJETA: el texto se parte, no se recorta
   ══════════════════════════════════════════════════════════════════════════
   material.css trae esta regla, sin matices:

     .card-footer span { white-space: nowrap; }

   Se escribio pensando en los indicadores cortos del panel original —"hace 4
   minutos", "+12 %"— para que el icono y su cifra no se separaran nunca. Pero
   apunta a CUALQUIER <span> del pie, y las plantillas de ficha de este proyecto
   envuelven todo el contenido del pie en un <span>. Todo lo que se ponga ahi
   hereda `nowrap` y se pinta en una sola linea, por larga que sea.

   Y ahi se junta con la segunda mitad del problema: `.card` lleva
   `overflow: hidden` —legitimo, es lo que respeta el borde redondeado—, asi
   que lo que sobresale no se ve. No hay barra, no hay puntos suspensivos, no
   hay indicio: la frase simplemente se corta a mitad de palabra.

   Medido en la ficha de Bastion: el aviso "Conectate a la VPN para poder
   reiniciar esta maquina o levantar una instancia privada" ocupaba 505 px en
   una sola linea dentro de un pie de 438 px. Los ultimos 67 px —"levantar una
   instancia privada"— quedaban fuera del recorte. El alumno leia una
   instruccion a medias, que es peor que no leer ninguna.

   Es la clase entera, no este aviso: cualquier frase que se ponga en el pie de
   cualquier tarjeta de la aplicacion tiene el mismo destino.

   Se corrige permitiendo el salto de linea. Partir es siempre preferible a
   recortar: una frase en dos lineas se lee entera; una frase recortada, no.
   Los indicadores cortos que motivaron el `nowrap` original siguen cabiendo en
   una linea por si mismos, asi que no cambian. */
.card-footer span,
.card-footer .stats {
  white-space: normal;
}
/* Y el propio aviso lo declara, para no depender de lo que herede del pie. */
.target-actions-vacio { white-space: normal; }

/* ══════════════════════════════════════════════════════════════════════════
   MENUS DESPLEGABLES
   ══════════════════════════════════════════════════════════════════════════
   material-dashboard.css pinta `.dropdown-menu a` con #333333, un gris casi
   negro pensado para un menu blanco. Sobre la superficie oscura de la consola
   eso da 1.14:1 — medido en el menu "Acciones de perfil": sus cuatro opciones
   eran practicamente invisibles y solo se adivinaban al pasar el raton.

   Se corrige la CLASE, no ese menu: cualquier desplegable de la aplicacion
   —acciones de maquina, pistas, avisos, valoraciones— comparte el problema. */
.dropdown-menu {
  background: var(--bg-1) !important;
  border: 1px solid var(--line) !important;
  border-radius: var(--r-md) !important;
  box-shadow: 0 18px 40px -12px oklch(0 0 0 / 0.6) !important;
  padding: 5px !important;
}
.dropdown-menu > li > a,
.dropdown-menu .dropdown-item,
.dropdown-menu a {
  color: var(--fg-dim) !important;
  border-radius: var(--r-sm);
  padding: 8px 11px !important;
  font-size: 12.5px;
  min-height: 34px;
  display: flex; align-items: center; gap: 8px;
  white-space: nowrap;
}
.dropdown-menu > li > a:hover,
.dropdown-menu .dropdown-item:hover,
.dropdown-menu a:hover,
.dropdown-menu a:focus-visible {
  background: var(--bg-2) !important;
  color: var(--fg) !important;
  text-decoration: none;
}

/* ──────────────────────────────────────────────────────────────────────────
   ESTADO SELECCIONADO / ACTIVO: el color del texto y el del fondo se deciden
   JUNTOS
   ──────────────────────────────────────────────────────────────────────────
   Esta regla repara una regresion introducida por la regla de arriba.

   material-dashboard.css empareja fondo y texto en una sola declaracion:

     .dropdown-item.active, .dropdown-item:active   { background:#94c11f; color:#fff }
     .dropdown-menu .dropdown-item:focus            { background:#94c11f; color:#fff }

   Al forzar `color: var(--fg-dim) !important` sobre TODO enlace de menu para
   arreglar la ilegibilidad sobre fondo oscuro, ese `!important` gano al
   `color:#fff` de material —que no lo lleva— pero no toco su `background`.
   El par quedo roto: fondo verde lima con texto gris claro.

   Medido sobre el desplegable de dificultad de la ficha de maquina:
   oklch(0.76 0.012 260) sobre rgb(148,193,31) = **1.01:1**. El minimo AA es
   4.5:1. La opcion elegida era literalmente invisible, que es justo lo que se
   ve en la captura: el hueco verde donde deberia decir "Avanzado".

   No es un fallo de ese desplegable: `!important` sobre la clase base gana en
   TODOS los estados de TODOS los menus de la aplicacion. Tambien afectaba a
   `:focus`, o sea al recorrido con teclado (WCAG 2.4.7): el elemento enfocado
   se pintaba de verde y perdia su texto.

   La correccion declara los estados con el mismo criterio que el resto del
   tema —acento del sistema de diseno, tinta oscura encima— en lugar de dejar
   que dos hojas decidan la mitad cada una. */
/* SELECCIONADO: se marca, no se grita.
   La primera version rellenaba la fila entera con el acento y ponia tinta
   oscura encima. Medido daba 15,27:1, o sea que "pasaba" de sobra — y aun asi
   estaba mal: un bloque de verde saturado dentro de un menu oscuro se lleva
   toda la atencion de la pantalla y el texto se ahoga dentro del color. Pasar
   una metrica no es lo mismo que ser legible.
   Ahora el estado se comunica con un fondo tenue del acento, el texto en el
   propio acento y algo mas de peso: se distingue de un vistazo sin competir
   con el resto de la interfaz. */
.dropdown-menu > li.active > a,
.dropdown-menu > li.selected > a,
.dropdown-menu li.active > a,
.dropdown-menu li.selected > a,
.dropdown-menu .dropdown-item.active,
.dropdown-menu .dropdown-item.selected,
.dropdown-menu .dropdown-item:active,
.dropdown-menu a.active,
.dropdown-menu a:active {
  background: var(--accent-bg) !important;
  color: var(--accent) !important;
  font-weight: 600;
  text-decoration: none;
  box-shadow: inset 2px 0 0 var(--accent) !important;
}
/* El foco de teclado no se pinta como "seleccionado": se pinta como foco, con
   un contorno visible. Antes compartian estilo y no se distinguia cual era
   cual al recorrer el menu con el tabulador. */
.dropdown-menu .dropdown-item:focus,
.dropdown-menu .dropdown-item:focus-visible,
.dropdown-menu a:focus,
.dropdown-menu a:focus-visible {
  background: var(--bg-2) !important;
  color: var(--fg) !important;
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  text-decoration: none;
}
/* Los iconos y la marca de verificacion viven dentro del enlace: si no se
   invierten tambien, quedan del color del acento sobre el propio acento. */
.dropdown-menu > li.active > a i,
.dropdown-menu > li.selected > a i,
.dropdown-menu .dropdown-item.active i,
.dropdown-menu .dropdown-item.selected i,
.dropdown-menu .dropdown-item:focus i,
.dropdown-menu a:focus i,
.dropdown-menu .selected .check-mark,
.dropdown-menu .active .check-mark {
  color: var(--accent) !important;
}
/* El menu de bootstrap-select repite el nombre de la opcion en un <small>
   con blanco al 50 % de opacidad: sobre el acento claro desaparece. */
.dropdown-menu li.active small,
.dropdown-menu li.selected small,
.dropdown-menu .dropdown-item.active small,
.dropdown-menu .dropdown-item:focus small {
  color: var(--fg-mute) !important;
  opacity: 1;
}
.dropdown-menu .divider, .dropdown-menu .dropdown-divider {
  background: var(--line) !important; height: 1px; margin: 4px 0;
}
.dropdown-menu i { color: var(--accent); flex: 0 0 auto; }

/* ══════════════════════════════════════════════════════════════════════════
   CAMPOS DE TEXTO: el color lo decide el tema, no la vista
   ══════════════════════════════════════════════════════════════════════════
   material.css trae esta regla:

     .challenge_answer_form input[type="text"].form-control { color: black; }

   Especificidad (0,3,1), escrita para el tema claro original. Medido en
   /challenge/4: el campo donde se escribe la respuesta tenia color negro puro
   sobre el fondo oscuro de la tarjeta —contraste 1.07:1— asi que el alumno
   escribia su respuesta y la veia desaparecer. El placeholder, igual.

   Un `<input>` NO hereda `color` de su contenedor: si ninguna regla de autor
   gana, el navegador aplica el negro de su hoja de usuario-agente. Por eso el
   fallo no se limita a este selector: cualquier campo que quede sin regla
   efectiva sale negro sobre negro.

   Se cierra la clase entera —todo campo editable de la aplicacion— en vez de
   neutralizar ese selector concreto. El !important sigue la convencion ya
   documentada: la regla de origen vive en una hoja vendida y su especificidad
   esta inflada a proposito.
   ══════════════════════════════════════════════════════════════════════════ */
input[type="text"], input[type="email"], input[type="password"], input[type="search"],
input[type="url"], input[type="tel"], input[type="number"], input:not([type]),
textarea, select, .form-control {
  color: var(--fg) !important;
  -webkit-text-fill-color: var(--fg);
  caret-color: var(--accent);
}
input::placeholder, textarea::placeholder, .form-control::placeholder {
  color: var(--fg-mute) !important;
  -webkit-text-fill-color: var(--fg-mute);
  opacity: 1;
}
/* El autorrelleno del navegador tambien pinta su propio fondo y color. */
input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--fg);
  -webkit-box-shadow: 0 0 0 1000px var(--bg-1) inset;
  caret-color: var(--accent);
}
/* Un campo deshabilitado se distingue por el tono, no por ser ilegible. */
input:disabled, textarea:disabled, select:disabled, .form-control:disabled {
  color: var(--fg-mute) !important;
  -webkit-text-fill-color: var(--fg-mute);
}

/* El formulario de respuesta de retos: caja propia, no un input desnudo.
   Llevaba `border: 0; padding: 0` en linea, asi que no se veia donde empezaba
   el campo. */
.challenge_answer_form .form-control {
  background: var(--bg-1) !important;
  border: 1px solid var(--line) !important;
  border-radius: var(--r-sm) !important;
  padding: 10px 12px !important;
  min-height: 42px;
  font-family: var(--mono);
}
.challenge_answer_form .form-control:focus {
  border-color: var(--accent) !important;
  box-shadow: 0 0 0 3px var(--accent-bg) !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   IMAGEN DE PERFIL
   ══════════════════════════════════════════════════════════════════════════
   EL MARCADO REAL, que es donde estaba el error de puntería:

     <div class="card-avatar bg-primary">   <- circulo de 130x130, FONDO LIMA
       <a href="/profile/me">               <- elemento EN LINEA
         <img class="img" src="...">        <- 255x255 de origen
       </a>
     </div>

   EL DEFECTO ORIGINAL. material-dashboard.css disena este avatar para
   sobresalir por encima de la tarjeta:

     .card-profile .card-avatar { margin: -50px auto 0; overflow: hidden;
                                  border-radius: 50%; }

   Ese margen negativo tiene sentido cuando la tarjeta lleva una banda de
   color arriba contra la que el avatar se recorta a proposito. Esta tarjeta
   no la tiene, asi que los 50 px sobrantes caian fuera del borde superior y
   `.card { overflow: hidden }` —legitimo, respeta la esquina redondeada— los
   cortaba. Ese era el recorte que se reporto.

   POR QUE EL PRIMER INTENTO NO SIRVIO. Se escribio una regla para
   `.card .profile img`, `.profile .avatar img` e `img.profile-avatar`.
   NINGUNO de esos selectores coincide con este marcado: aqui el contenedor es
   `.card-avatar`, no `.profile`, y la imagen no lleva ninguna de esas clases.
   La regla de la imagen no llego a aplicarse jamas.

   Lo unico que si aplico fue la del contenedor, y empeoro las cosas:

     .card-profile .card-avatar { overflow: visible; padding-top: 18px; margin-top: 0 }

    - `overflow: visible` desactivo la MASCARA. Sin ella, la imagen cuadrada
      deja de recortarse en circulo y asoma por las esquinas.
    - `padding-top: 18px` empujo la imagen 18 px hacia abajo DENTRO del
      circulo, dejando a la vista una franja de 18 px del fondo del contenedor
      —que es `bg-primary`, el lima #94c11f de la paleta antigua— y sacando
      otros 18 px de imagen por debajo.

   Esa franja verde es exactamente lo que se ve en la captura. El sintoma
   cambio de "recortada por arriba" a "cercada de verde", pero la causa —una
   imagen y su contenedor que no coinciden— seguia entera.

   LA CORRECCION. Se atacan las dos mitades a la vez, que es lo que faltaba:

    1. El contenedor deja de sobresalir de la tarjeta (`margin-top: 0`), asi
       que ya no hay nada que `.card` pueda cortar. Esta parte del intento
       anterior era correcta y se conserva.
    2. Se RECUPERA la mascara circular (`overflow: hidden` + `border-radius`),
       que es lo que convierte una imagen cuadrada en un avatar redondo.
    3. La imagen PASA A LLENAR el contenedor en vez de quedar empujada dentro
       de el: `width/height: 100%` con `object-fit: cover`. Al cubrirlo entero
       ya no puede asomar el fondo por ningun lado, con franja o sin ella.
    4. El enlace intermedio pasa a `display: block`. Siendo en linea, la
       imagen se alineaba a la linea base y arrastraba el hueco del trazo
       descendente: 18 px de desfase que no venian de ninguna regla, sino de
       como se compone el texto.
    5. El fondo del hueco deja de ser lima. Si alguna vez asoma —una imagen
       que tarda o que no carga— debe verse el color de la superficie, no un
       verde de otra paleta.

   Se aplica a la CLASE `.card-avatar`, no a esta pantalla: es el mismo
   componente que usan las fichas de perfil de cualquier vista. */
.card-profile .card-avatar,
.card .card-avatar {
  width: 130px;
  height: 130px;
  margin: 0 auto;
  padding: 0;
  overflow: hidden;
  border-radius: 50%;
  /* El `!important` se mantiene como red: la clase `bg-primary` ya se ha
     quitado del marcado de _card.php, pero el mismo componente aparece en otras
     vistas que aun podrian traerla, y las utilidades de color de Bootstrap se
     declaran con `!important`. Sin esto, ahi volveria a verse el lima. */
  background: var(--bg-2) !important;
  border: 2px solid var(--line);
  display: block;
  flex: 0 0 auto;
}
.card-profile .card-avatar > a,
.card .card-avatar > a {
  display: block;
  width: 100%;
  height: 100%;
  line-height: 0;
}
.card-profile .card-avatar img,
.card .card-avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

/* Los otros contenedores de avatar del proyecto, por si alguna vista usa este
   otro marcado. Se mantienen con el mismo criterio: cubrir, no deformar. */
.card .profile img,
.profile .avatar img,
img.profile-avatar {
  display: block;
  width: 120px; height: 120px;
  max-width: 100%;
  margin: 0 auto 12px;
  object-fit: cover;
  border-radius: 50%;
  border: 2px solid var(--line);
  background: var(--bg-1);
}
.card .profile {
  overflow: visible;
  margin-top: 0;
}

/* Estado vacio en linea: es la ausencia de un dato, no un dato. */
.estado-vacio { color: var(--fg-mute); font-size: 12.5px; font-style: normal; }

/* Miniatura alineada a la derecha dentro de una fila de actividad. */
.thumb-derecha { float: right; margin-left: 10px; display: inline-flex; }

/* Lista de lo que falta para poder generar el perfil de VPN. Es un diagnostico
   dirigido a quien administra el rango, no una decoracion: se pinta legible y
   separada del parrafo que la introduce. */
.acceso-vpn__faltantes {
  margin: 10px 0 0; padding: 0 0 0 18px;
  color: var(--fg-mute); font-size: 12.5px; line-height: 1.55;
}
.acceso-vpn__faltantes li { margin: 3px 0; }

/* Un aviso del menu de la campana es contenido, no un enlace: la tabla
   `notification` no tiene destino al que enlazar, el aviso ES el contenido.
   Se pinta con titulo y cuerpo, sin cursor de mano ni estado de pulsable. */
.dropdown-menu .aviso {
  display: block; cursor: default;
  padding: 9px 11px !important; white-space: normal;
  border-radius: var(--r-sm);
}
.dropdown-menu .aviso:hover { background: var(--bg-2); }
.aviso-titulo { display: block; color: var(--fg); font-size: 12.5px; line-height: 1.35; }
.aviso-cuerpo { display: block; color: var(--fg-mute); font-size: 12px; line-height: 1.45; margin-top: 2px; }

/* Tope de altura de los menús de lista de la barra (avisos y pistas). Sin esto
   el desplegable crece un renglón por elemento: con ~20 avisos medía 1571px
   —1.8x la pantalla— y los de abajo quedaban fuera del viewport, inalcanzables
   porque el menú no desplazaba. Ahora se acota a lo que cabe y desborda con
   scroll propio; `overscroll-behavior: contain` evita que el scroll arrastre la
   página al llegar al final. */
.dropdown-lista {
  max-height: min(70vh, 26rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Disparador del menu de acciones de una maquina. Usa el mismo control que los
   menus de la barra superior (.icon-btn) para que un icono pulsable se vea
   igual en toda la consola. Antes era un <a> en linea de 18px sin relleno ni
   fondo, con el icono saliendose por abajo y un caret invisible al lado. */
.target-actions { display: inline-flex; margin-left: auto; }
.target-actions-toggle {
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--fg-mute);
}
.target-actions-toggle:hover,
.target-actions-toggle[aria-expanded="true"] { color: var(--accent); }
.target-actions-toggle i { font-size: 15px; line-height: 1; }

/* ══════════════════════════════════════════════════════════════════════════
   SELECTORES DE FORMULARIO (bootstrap-select)
   ══════════════════════════════════════════════════════════════════════════
   Las vistas pedian `data-style="btn-info"`, o sea que el disparador del
   selector se pintaba como un BOTON DE ACCION de color: turquesa solido con
   texto blanco, de la paleta de material-dashboard. En un formulario oscuro,
   junto a campos de texto discretos, eso grita — y ademas miente sobre lo que
   es: no es un boton que ejecute nada, es un campo que elige un valor.

   Intentar corregirlo redefiniendo `.btn.btn-info` no bastaba, y ahi estuvo mi
   error: material-dashboard pinta CADA ESTADO con mas especificidad que la
   regla base.

     .open > .btn.btn-info.dropdown-toggle    0,3,1   <- gana
     .btn.btn-info:hover, :focus, .active     0,3,0   <- gana
     .btn.btn-info            (la mia)        0,2,0

   Asi que en reposo se veia bien y al abrirlo o enfocarlo volvia el turquesa.
   Medido con elementFromPoint sobre el pixel real: rgb(0,174,197).

   La correccion de raiz no es pelear con la cascada: es dejar de pedir un
   estilo de boton. Las vistas ahora piden `btn-campo`, una clase propia que no
   existe en ninguna hoja heredada, asi que no hay nada que sobreescribir. Se
   pinta como los demas campos del formulario. */
.bootstrap-select > .btn.btn-campo,
.bootstrap-select > .btn.btn-campo:hover,
.bootstrap-select > .btn.btn-campo:focus,
.bootstrap-select > .btn.btn-campo:active,
.bootstrap-select > .btn.btn-campo.active,
.bootstrap-select.open > .btn.btn-campo,
.bootstrap-select.show > .btn.btn-campo {
  background: var(--bg-2) !important;
  color: var(--fg) !important;
  border: 1px solid var(--line) !important;
  border-radius: var(--r-sm) !important;
  box-shadow: none !important;
  text-transform: none;
  font-weight: 500;
  letter-spacing: 0;
  padding: 10px 12px !important;
  min-height: 42px;
  display: flex; align-items: center;
}
.bootstrap-select > .btn.btn-campo:focus,
.bootstrap-select.open > .btn.btn-campo {
  border-color: var(--accent) !important;
  box-shadow: 0 0 0 3px var(--accent-bg) !important;
}
.bootstrap-select > .btn.btn-campo .filter-option,
.bootstrap-select > .btn.btn-campo .filter-option-inner,
.bootstrap-select > .btn.btn-campo .filter-option-inner-inner {
  color: var(--fg) !important;
  text-align: left;
}
/* La flecha del selector, del color del texto secundario y no del acento. */
.bootstrap-select > .btn.btn-campo::after { border-top-color: var(--fg-mute) !important; }

/* ── Insignias compartibles del perfil ─────────────────────────────────── */
.insignias { text-align: left; }
.insignias-intro, .insignias-pista { color: var(--fg-mute); font-size: 13.5px; margin: 4px 0 16px; }
.insignias-lista { display: flex; flex-direction: column; gap: 14px; }
.insignia {
  display: flex; gap: 16px; align-items: flex-start;
  background: var(--bg-1); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 14px;
}
.insignia-vista { flex: 0 0 auto; max-width: 260px; }
.insignia-vista img {
  display: block; width: 100%; height: auto;
  border-radius: var(--r-sm); border: 1px solid var(--line-soft); background: var(--bg-2);
}
.insignia-datos { flex: 1 1 auto; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.insignia-datos strong { color: var(--fg); font-size: 14.5px; }
.insignia-datos > span { color: var(--fg-mute); font-size: 13px; }
.insignia-copiar { display: flex; gap: 8px; margin-top: 8px; }
.insignia-copiar input {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--mono); font-size: 12px;
  background: var(--bg-2) !important; color: var(--fg-dim) !important;
  border: 1px solid var(--line) !important; border-radius: var(--r-sm) !important;
  padding: 8px 10px !important;
}
.btn-copiar-insignia { flex: 0 0 auto; white-space: nowrap; }
.btn-copiar-insignia.copiado { border-color: var(--accent) !important; color: var(--accent) !important; }
@media (max-width: 640px) {
  .insignia { flex-direction: column; }
  .insignia-vista { max-width: 100%; }
}
/* Una insignia que es PAGINA y no imagen: se enlaza, no se incrusta. */
.insignia-enlace {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  width: 160px; height: 104px; text-decoration: none;
  background: var(--bg-2); border: 1px dashed var(--line-bright);
  border-radius: var(--r-sm); color: var(--fg-dim); font-size: 12.5px;
}
.insignia-enlace:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
.insignia-enlace-ico { font-size: 26px; line-height: 1; }

/* ══════════════════════════════════════════════════════════════════════════
   EQUIPOS
   ══════════════════════════════════════════════════════════════════════════
   Los filtros eran tres botones de tres colores distintos —gris, turquesa y
   naranja— sin ninguna relacion con lo que filtran, y ninguno indicaba cual
   estaba activo. El color en una interfaz significa algo: naranja es
   advertencia, y «Solo por invitacion» no es una advertencia.

   Un grupo de segmentos dice lo que son: opciones excluyentes de la misma
   familia, donde solo una esta activa. Mismo peso visual entre ellas, y el
   estado activo se marca con el acento del sistema. */
.equipos-barra {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 14px; margin-bottom: 16px;
}
.segmentos {
  display: inline-flex; padding: 3px;
  background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--r-pill);
}
.segmento {
  display: inline-flex; align-items: center;
  padding: 7px 15px; border-radius: var(--r-pill);
  font-size: 13px; font-weight: 500; line-height: 1;
  color: var(--fg-mute); text-decoration: none;
  transition: color .15s var(--t), background .15s var(--t);
}
.segmento:hover { color: var(--fg); text-decoration: none; }
.segmento.activo { background: var(--bg-3); color: var(--fg); font-weight: 600; }
.segmento:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.equipos-intro { color: var(--fg-mute); font-size: 14px; max-width: 68ch; margin: 0 0 18px; }
.equipos-lista { margin-top: 4px; }

/* Un listado vacio es informacion, no una alarma. */
.estado-vacio-bloque {
  color: var(--fg-mute); font-size: 14px;
  background: var(--bg-1); border: 1px dashed var(--line);
  border-radius: var(--r-md); padding: 22px; text-align: center; margin: 0;
}

/* La tarjeta de equipo comparte el aspecto de las demas fichas de la consola. */
.team-card { width: 100%; }
.team-card .card-icon img { border: 2px solid var(--line); background: var(--bg-2); }
.team-card .card-title { font-size: 17px; }

/* Botones y etiquetas de la tarjeta de equipo. `btn-info` y `btn-warning`
   pintaban turquesa y naranja segun si el equipo era abierto o por invitacion
   —informacion que ya da la etiqueta de al lado— y el naranja se leia como una
   advertencia que no lo era. */
.btn-equipo { width: 100%; justify-content: center; margin-top: 10px; }
.etiqueta-equipo {
  display: inline-block; padding: 3px 9px; border-radius: var(--r-pill);
  background: var(--bg-3); color: var(--fg-mute);
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase;
}
.equipo-lider { color: var(--fg-mute); font-size: 13px; margin: 8px 0 0; }
.equipo-lider span { color: var(--fg-faint); }

/* Tarjeta de perfil compartible, en los ajustes. */
.tarjeta-perfil-intro { color: var(--fg-mute); font-size: 13.5px; margin: 0 0 14px; }
.tarjeta-perfil-vista { margin-bottom: 12px; }
.tarjeta-perfil-vista img {
  display: block; width: 100%; max-width: 420px; height: auto;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--bg-2);
}

/* ── Tarjetas que contienen un menú abierto ────────────────────────────────
 * `.card` declara `overflow: hidden` (academia-design.css:626) y
 * `.card.card-stats` lo repite (cyberconquer.css:962). Es legítimo: es lo que
 * respeta la esquina redondeada y enmascara los fondos cuadrados de
 * .card-header y .card-footer. Pero recorta a TODO descendiente, y el menú de
 * acciones de la máquina es `position: absolute` DENTRO de la tarjeta: nace
 * cortado.
 *
 * Se levanta el recorte SOLO mientras hay un menú desplegado.
 * `.dropdown-menu.show` únicamente existe mientras Bootstrap lo tiene abierto,
 * así que en reposo la tarjeta sigue recortando como siempre.
 *
 * EL z-index NO ES DECORATIVO. El `transform` del hover de la tarjeta
 * (.card.card-stats:hover en cyberconquer.css:967, y .card:has(.btn):hover en
 * academia-design.css:1426) convierte la tarjeta en un contexto de apilamiento
 * con z-index `auto`; sin esto, el menú liberado se pinta DEBAJO de la tarjeta
 * siguiente y el clic se lo lleva la de al lado. La tarjeta ya es
 * `position: relative`, así que el z-index surte efecto sin tocar nada más.
 *
 * Especificidad: `:has()` aporta la de su argumento más específico, así que
 * `.card:has(.dropdown-menu.show)` es (0,3,0) y gana a las dos reglas de
 * arriba sin necesidad de `!important`.
 *
 * Cubre la CLASE: las 39 plantillas con `class="card"` del proyecto, incluido
 * el menú de acciones de perfil, que hoy no se ve cortado solo porque debajo
 * hay sitio suficiente. */
.card:has(.dropdown-menu.show) {
  overflow: visible;
  z-index: 20;
}

/* ── Filtros del listado de máquinas ───────────────────────────────────────
 * Reutilizan `.segmentos`/`.segmento`, los mismos que los filtros de Equipos.
 * Dos controles que hacen lo mismo —acotar una lista— deben verse y
 * comportarse igual; inventar aquí un tercer estilo de botón solo obligaría al
 * alumno a aprender dos veces la misma cosa.
 *
 * `.segmentos` es inline-flex y no se parte, así que en una pantalla estrecha
 * la fila de niveles se saldría. Se le deja desbordar en su propio contenedor
 * en vez de romper la maqueta: la barra entera nunca provoca scroll
 * horizontal de la página. */
.filtros-maquinas {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 0 0 18px;
}
.filtro-linea {
  display: flex;
  align-items: center;
  gap: 12px;
  /* Solo esta fila se desplaza si no cabe; el <body> no. */
  overflow-x: auto;
  /* `overflow-x: auto` hace que `overflow-y` compute a `auto` tambien, y con
   * altura automatica eso puede sacar una barra vertical parasita de 2-3 px
   * cuando aparece la horizontal. Se fija explicitamente. */
  overflow-y: hidden;
  /* 4 px, que es lo que ocupa el contorno de foco con su offset; con 2 px
   * quedaba recortado por el propio overflow. */
  padding: 4px 0;
  scrollbar-width: thin;
}
.filtro-titulo {
  flex: 0 0 auto;
  min-width: 5.5rem;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--fg-mute);
}
.filtro-linea .segmentos { flex: 0 0 auto; }

/* LAS PASTILLAS NO LLEVAN COLOR POR NIVEL.
 *
 * La primera version pintaba de color la pastilla activa de «Nivel» y dejaba
 * gris la de «Categoria», que es la regla `.segmento.activo` normal. A 10 px de
 * distancia habia dos filas de controles identicos que respondian distinto al
 * mismo gesto: el alumno aprendia «color = seleccionado» en la primera y no lo
 * encontraba en la segunda.
 *
 * Y el color estaba donde menos falta hace: en la pastilla que el alumno ACABA
 * de pulsar, o sea el unico dato que ya conocia. Donde si distingue algo es en
 * el listado, separando una fila de las de al lado; ahi esta ahora,
 * en `.meta-nivel`. Las dos filas de filtros usan la misma pastilla gris. */

/* La segunda linea bajo el nombre de la maquina en el listado: nivel y
 * categoria en texto, visibles en TODOS los anchos. Antes el nivel solo existia
 * como icono en una columna `d-none d-xl-table-cell`, oculta por debajo de
 * 1200 px, y la categoria no aparecia en ninguna columna. */
.target-meta {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--fg-mute);
}
/* El color acompana a la palabra, nunca la sustituye: la fila dice
 * «Intermedia» ademas de pintarse de ambar. La escala va de frio a caliente. */
.meta-nivel { font-weight: 600; }
.meta-nivel.nivel-0 { color: #7fd4f5; }
.meta-nivel.nivel-1 { color: #4fd6a8; }
.meta-nivel.nivel-2 { color: #f0c04f; }
.meta-nivel.nivel-3 { color: #f79a54; }
.meta-nivel.nivel-4 { color: #f07070; }
.meta-nivel.nivel-5 { color: #c78af5; }
.meta-nivel.nivel-6 { color: #eb6a90; }

/* `.filtro-activo` y `.quitar-filtro` YA estan definidos mas arriba en este
 * mismo archivo (~1260). Aqui habia un segundo bloque que los volvia a
 * declarar 880 lineas despues, y el resultado no era ninguna de las dos
 * intenciones sino un hibrido: sobrevivia el `font-family: var(--mono)` del
 * primero y se imponia el `color: var(--fg)` del segundo, o sea monoespaciado
 * en blanco. Y `.quitar-filtro { margin-left: 10px }` era codigo muerto: lo
 * pisaba `.filtro-activo .quitar-filtro { margin-left: auto }`, que es mas
 * especifico. Se borra: si hay que cambiar algo, se cambia arriba. */

/* 760 y no 700: es el punto de corte que ya usa el resto del archivo
 * (1100, 980, 760, 640). Con 700 habia una franja entre 701 y 760 en la que
 * los demas componentes ya habian colapsado y el filtro seguia horizontal. */
@media (max-width: 760px) {
  /* En vertical el rótulo encima ahorra el ancho que necesitan las pastillas. */
  .filtro-linea { flex-direction: column; align-items: flex-start; gap: 6px; }
  .filtro-titulo { min-width: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════
 * REDISEÑO DE RETOS, TUTORIALES, MARCADORES Y REDES
 *
 * Las cuatro vistas arrastraban los mismos defectos: el titulo con el
 * nombre del sitio pegado y en ingles, un <h2> repitiendolo bajo el <h1>,
 * textos sin traducir escritos en el PHP, <hr /> de separador y el estado
 * vacio pintado como advertencia.
 *
 * El CSS se anade aqui de una vez, y no lo escribio cada vista por su
 * cuenta, porque esta hoja es COMPARTIDA: cuatro cambios simultaneos sobre
 * el mismo archivo se pisan entre si. Comprobado antes de fusionar: 77
 * selectores nuevos, 0 colisiones con los 193 que ya habia.
 * ═══════════════════════════════════════════════════════════════════════ */
/* ══════ RETOS (/CHALLENGES Y /CHALLENGE/N) ══════ */
/* ── Retos (/challenges y /challenge/N) ───────────────────────────────────
 * La rejilla sustituye a `row` + `col-xl-4 col-sm-6` repetido en cada tarjeta.
 * Con columnas de Bootstrap la altura de cada tarjeta la marcaba su propio
 * contenido, así que en una fila con un reto de nombre largo y otro corto las
 * fichas quedaban desalineadas; `grid` iguala la fila entera sin que la
 * plantilla tenga que saber nada de anchos.
 *
 * `auto-fill` con mínimo de 260 px: de 1 a 4 columnas según el ancho, sin
 * puntos de corte escritos a mano. */
.retos-resumen {
  color: var(--fg-mute);
  font-size: 13.5px;
  margin: 0 0 16px;
}
.retos-rejilla {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 16px;
}
/* El estado vacío es hijo directo de la rejilla: ocupa el ancho completo en
 * vez de quedarse encogido en la primera columna. */
.retos-rejilla > .estado-vacio-bloque { grid-column: 1 / -1; }

.retos-tarjeta {
  display: flex;
  flex-direction: column;
  /* `.card` trae `margin-bottom: 18px`, que dentro de una rejilla se suma al
   * `gap` y solo por abajo: las filas quedaban separadas 34 px y las columnas
   * 16 px. El espaciado lo pone el `gap`. */
  margin-bottom: 0;
  padding: 16px 18px 14px;
  position: relative;
  transition: border-color .15s var(--t), transform .15s var(--t), box-shadow .15s var(--t);
}
.retos-tarjeta:hover {
  border-color: var(--line-bright);
  transform: translateY(-2px);
  box-shadow: var(--shadow-2);
}
/* Resuelto: un filo de acento a la izquierda. Es refuerzo, no el dato —la
 * palabra «Resuelto» está siempre escrita en la etiqueta de arriba. */
.retos-tarjeta--resuelto { border-left: 3px solid var(--accent-dim); }

.retos-cabecera {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
  min-height: 20px;
}
.retos-icono { color: var(--fg-mute); font-size: 15px; line-height: 1; }
.retos-icono i, .retos-icono svg { font-size: inherit; }

/* El estado va a la derecha de su fila y lleva SIEMPRE la palabra: el color y
 * el icono solo la acompañan. */
.retos-estado {
  margin: 0 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.retos-estado--resuelto { color: var(--accent-1); }
.retos-estado--en-curso { color: var(--warn); }

.retos-nombre {
  margin: 0 0 10px;
  font-size: 16px;
  line-height: 1.3;
  font-weight: 600;
}
.retos-nombre a { color: var(--fg); text-decoration: none; }
.retos-nombre a:hover { color: var(--accent-1); text-decoration: none; }
/* ENLACE ESTIRADO: un solo enlace por tarjeta —el nombre, que es único y por
 * tanto se entiende fuera de contexto— pero toda la ficha es zona de clic.
 * Antes había dos enlaces al mismo destino y el segundo se llamaba «Ir al
 * reto» en las seis tarjetas. */
.retos-nombre a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}
/* El foco tiene que verse en la TARJETA, que es lo que se activa; sobre el
 * texto del título solo se vería un rectángulo suelto de una línea. */
.retos-nombre a:focus-visible { outline: none; }
.retos-tarjeta:has(.retos-nombre a:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.retos-etiquetas {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 10px;
}
.retos-etiqueta {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  background: var(--bg-2);
  border: 1px solid var(--line-soft);
  color: var(--fg-dim);
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .05em;
  white-space: nowrap;
}
/* Misma escala de color que `.meta-nivel` en el listado de máquinas, para que
 * «Intermedio» aquí y «Intermedia» allí se reconozcan como lo mismo. El color
 * NUNCA sustituye a la palabra: la etiqueta la lleva escrita. */
.retos-nivel { font-weight: 600; }
.retos-nivel--facil      { color: #4fd6a8; }
.retos-nivel--intermedio { color: #f0c04f; }
.retos-nivel--avanzado   { color: #f79a54; }
.retos-nivel--extremo    { color: #f07070; }

.retos-datos {
  margin: 0;
  color: var(--fg-mute);
  font-size: 12px;
}

/* Aviso visual de la zona de clic, no un enlace: el enlace real es el título
 * estirado sobre la tarjeta, así que esto va aria-hidden en el HTML. */
.retos-abrir {
  margin: 14px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--line-soft);
  color: var(--fg-mute);
  font-size: 12.5px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.retos-flecha { transition: transform .15s var(--t); }
.retos-tarjeta:hover .retos-abrir { color: var(--accent-1); }
.retos-tarjeta:hover .retos-flecha { transform: translateX(3px); }

/* ── Ficha de un reto ──────────────────────────────────────────────────── */
.retos-ficha {
  background: var(--card-grad);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  padding: 18px;
  margin-bottom: 18px;
}
.retos-ficha .retos-etiquetas { margin-bottom: 14px; }
.retos-descripcion { color: var(--fg-dim); font-size: 14.5px; line-height: 1.6; }
.retos-descripcion > :last-child { margin-bottom: 0; }

/* Material adjunto. Solo aparece cuando el reto tiene archivo; hoy ninguno lo
 * tiene, así que en la práctica no se pinta nunca todavía. */
.retos-adjunto {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 14px;
  padding: 10px 12px;
  background: var(--bg-1);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-sm);
  font-size: 13px;
}
.retos-adjunto i { color: var(--fg-faint); }
.retos-adjunto-rotulo {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.retos-adjunto a { font-family: var(--mono); font-size: 12.5px; }

.retos-valoracion { margin-bottom: 18px; }
.retos-preguntas { margin-top: 4px; }

@media (max-width: 640px) {
  /* En vertical una sola columna: con 260 px de mínimo la rejilla ya lo hace
   * sola, pero la tarjeta gana aire al reducir el relleno. */
  .retos-tarjeta { padding: 14px 15px 12px; }
}

/* ══════ TUTORIALES — /TUTORIALS (LISTADO) Y /TUTORIAL/&LT;ID&GT; (GUÍA) ══════ */
/* ══════════════════════════════════════════════════════════════════════════
   TUTORIALES (guías)
   ══════════════════════════════════════════════════════════════════════════
   El listado no tenía estilo NINGUNO. Las tarjetas se pintaban con
   `class="tutorial-view well"`: `well` es de Bootstrap 3 y en este proyecto no
   existe —la única aparición en todo el CSS cargado es un `:hover` suelto en
   material.css:244—, así que las tres guías salían como texto corrido, sin
   fondo, sin borde y sin nada que dijera dónde acaba una y empieza la
   siguiente.

   Se pintan con el mismo lenguaje que las demás fichas de la consola
   (`.machine-card`, `.user-card`): superficie elevada, borde de un píxel,
   esquina de --r-md y el acento reservado para el estado activo. */

.tutoriales-lista {
  display: grid;
  gap: 12px;
  /* La medida manda: el resumen es prosa, y una línea de 140 caracteres se lee
     mal aunque el contenedor dé para tanto. 78ch es el ancho al que ya se
     ajusta el resto de texto largo de la consola. */
  max-width: 78ch;
}

/* `ListView` pinta SIEMPRE su <div class="summary">, vacío cuando se pide
   `summary => false`. Dentro de una rejilla con `gap` eso no es inofensivo:
   es un elemento más, y añade una separación de 12 px sobrante en la cabecera
   de la lista. */
.tutoriales-lista > .summary:empty { display: none; }

.tutoriales-guia {
  /* `relative` es lo que hace que el ::after del enlace se estire sobre la
     tarjeta entera; sin esto se posicionaría respecto al viewport. */
  position: relative;
  background: var(--bg-1);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 16px 18px;
  transition: border-color .18s var(--t), transform .18s var(--t), box-shadow .18s var(--t);
}
.tutoriales-guia:hover {
  border-color: oklch(0.55 0.18 143 / 0.4);
  transform: translateY(-2px);
  box-shadow: var(--shadow-1), 0 0 30px -14px var(--accent-glow);
}

/* Número de orden. Mismo tratamiento que el antetítulo de la cabecera de
   página (`.page-eyebrow`), porque cumple la misma función: situar antes de
   leer el título. */
.tutoriales-guia__orden {
  margin: 0 0 6px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}

.tutoriales-guia__titulo {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
}
.tutoriales-guia__titulo a { color: var(--fg); }
.tutoriales-guia__titulo a:hover { color: var(--accent); text-decoration: none; }

/* TARJETA ENTERA PULSABLE, PERO UN SOLO ENLACE.
   La alternativa —envolver toda la tarjeta en un <a>, como hace
   `.machine-card`— también funciona, pero el nombre accesible del enlace pasa
   a ser el bloque entero: número, título, resumen y recuento. Con la capa
   estirada, quien navega por enlaces oye «Conéctate al laboratorio» y quien
   usa el ratón puede pulsar en cualquier punto de la tarjeta. */
.tutoriales-guia__titulo a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--r-md);
}

/* El foco se dibuja sobre la TARJETA, no sobre las tres palabras del título:
   la zona pulsable es la tarjeta, y un contorno que no coincide con lo que se
   activa desorienta. */
.tutoriales-guia:has(.tutoriales-guia__titulo a:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.tutoriales-guia__titulo a:focus-visible { outline: none; }
/* Sin `:has` no habría NINGÚN indicador de foco, que es peor que uno mal
   colocado. Se devuelve el contorno al propio enlace. */
@supports not selector(:has(a)) {
  .tutoriales-guia__titulo a:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }
}

.tutoriales-guia__resumen {
  margin: 7px 0 0;
  color: var(--fg-dim);
  font-size: 14px;
  line-height: 1.6;
}

.tutoriales-guia__meta {
  margin: 10px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--line-soft);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg-mute);
}

/* ── Cuerpo de una guía ────────────────────────────────────────────────────
 * `formatter->asMarkdown()` devuelve su HTML dentro de `<div class="markdown">`
 * y NADIE estilaba esa clase: los encabezados salían con los márgenes del
 * navegador y las listas sin sangrar. Se estiliza acotado a los dos sitios de
 * esta pantalla, no `.markdown` a secas, que lo comparten novedades, metadatos
 * de máquina y writeups y no es de este trabajo tocarlos.
 *
 * `pre` y `code` YA vienen resueltos en academia-design.css:189-213; aquí solo
 * se ajusta la separación vertical. */
.tutoriales-cuerpo {
  max-width: 74ch;
}
.tutoriales-cuerpo .markdown > :first-child { margin-top: 0; }
.tutoriales-cuerpo .markdown h4 {
  margin: 26px 0 8px;
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--fg);
}
.tutoriales-cuerpo .markdown h5,
.tutoriales-cuerpo .markdown h6 {
  margin: 22px 0 6px;
  font-size: 15px;
  font-weight: 600;
  color: var(--fg);
}
.tutoriales-cuerpo .markdown p,
.tutoriales-cuerpo .markdown li {
  color: var(--fg-dim);
  font-size: 14.5px;
  line-height: 1.7;
}
.tutoriales-cuerpo .markdown p { margin: 0 0 12px; }
.tutoriales-cuerpo .markdown ul,
.tutoriales-cuerpo .markdown ol { margin: 0 0 14px; padding-left: 22px; }
.tutoriales-cuerpo .markdown li { margin: 4px 0; }
.tutoriales-cuerpo .markdown strong { color: var(--fg); font-weight: 600; }
.tutoriales-cuerpo .markdown pre { margin: 0 0 14px; }
.tutoriales-cuerpo .markdown blockquote {
  margin: 0 0 14px;
  padding-left: 14px;
  border-left: 3px solid var(--line-bright);
  color: var(--fg-mute);
}
/* Una tabla ancha se desplaza dentro de su bloque; la página nunca. */
.tutoriales-cuerpo .markdown table { display: block; overflow-x: auto; max-width: 100%; }

/* ── Preguntas de repaso ───────────────────────────────────────────────────
 * Antes cada pregunta era un <details> con el <summary> DETRÁS del contenido,
 * o sea un desplegable roto (ver el comentario de _task.php). Ahora son una
 * lista corta y visible, separada del cuerpo de la guía por una línea, porque
 * es otra cosa: se deja de leer y se empieza a responder. */
.tutoriales-preguntas {
  max-width: 74ch;
  margin-top: 30px;
  padding-top: 22px;
  border-top: 1px solid var(--line-soft);
}
.tutoriales-preguntas__titulo {
  margin: 0;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.tutoriales-preguntas__nota {
  margin: 6px 0 16px;
  color: var(--fg-mute);
  font-size: 13px;
  line-height: 1.6;
}
.tutoriales-preguntas__lista { display: grid; gap: 10px; }
.tutoriales-preguntas__lista > .summary:empty { display: none; }

.tutoriales-pregunta {
  background: var(--bg-1);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 14px 16px;
}
.tutoriales-pregunta__titulo {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--fg);
  line-height: 1.3;
}
/* El número no es solo color: es una cifra legible. Quien no distinga el verde
   sigue leyendo «1», «2», «3». */
.tutoriales-pregunta__num {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--r-pill);
  background: var(--accent-bg);
  border: 1px solid oklch(0.55 0.18 143 / 0.4);
  color: var(--accent);
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
}
.tutoriales-pregunta__cuerpo { margin-top: 8px; }
/* La pregunta es un párrafo suelto: hereda el cuerpo de la guía en tono más
   apagado, sin repetir toda la escala tipográfica. */
.tutoriales-pregunta__cuerpo .markdown p {
  margin: 0;
  color: var(--fg-dim);
  font-size: 14px;
  line-height: 1.65;
}

/* Progreso «Resueltas: n de N» dentro de la nota. Destaca sin gritar: mismo
   tamaño que la nota, en color de acento y sin salto de línea propio. */
.tutoriales-preguntas__progreso {
  display: inline-block;
  margin-left: 4px;
  color: var(--accent);
  font-weight: 700;
  white-space: nowrap;
}

/* Campo para responder la pregunta: ocupa el ancho disponible con el botón
   «Comprobar» al lado en pantalla ancha y debajo en móvil. */
.tutoriales-pregunta__form {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
/* QUIÉN CRECE ES EL ENVOLTORIO, NO EL INPUT.
   El tema material (bootstrap-material-design) envuelve el campo en un
   <span class="bmd-form-group"> por JS, y ESE span —no el input— es el hijo flex
   del formulario. Con el flex-grow puesto solo en el input, el envoltorio se
   quedaba con `flex: 0 1 auto` y el campo medía 245px dejando 290px de hueco a la
   derecha (justo lo que se veía). El grow va aquí en el hijo directo, cubriendo
   los dos casos: envuelto (.bmd-form-group) y sin envolver (el input suelto, si
   el JS de material no llegó a correr). */
.tutoriales-pregunta__form > .bmd-form-group,
.tutoriales-pregunta__form > input.tutoriales-pregunta__input {
  flex: 1 1 220px;
  min-width: 0;
}
/* El envoltorio de material trae margen propio (deja hueco a una etiqueta
   flotante que aquí no existe): se anula para que quede a la altura del botón. */
.tutoriales-pregunta__form .bmd-form-group { margin: 0; padding: 0; }
/* El campo lleva también la clase `form-control`, así que hereda del tema el
   borde, radio, padding, alto, tipografía y transición: se deja igual que el
   resto de campos. Se fuerza a llenar su envoltorio y a recede sobre la tarjeta.
   El selector `input.…__input` (0,1,1) dentro de `.…__form` da (0,2,1), por
   encima del genérico `input[type="text"]` (0,1,1). */
.tutoriales-pregunta__form input.tutoriales-pregunta__input {
  width: 100%;
  min-width: 0;
  background: var(--bg);        /* recede respecto a la tarjeta (--bg-1) */
}
/* Foco alineado con el resto de campos del tema (:focus-visible + anillo de 3px),
   en vez de un indicador propio más flojo. Solo por teclado, como la convención. */
.tutoriales-pregunta__form input.tutoriales-pregunta__input:focus-visible {
  outline: none;
  border-color: var(--accent-dim);
  box-shadow: 0 0 0 3px var(--accent-bg);
}
.tutoriales-pregunta__enviar { flex: none; }

/* Insignia de pregunta ya resuelta: sustituye al campo. Verde de acierto, con
   el icono a la izquierda; role="status" en el marcado la anuncia al lector. */
.tutoriales-pregunta__resuelta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0 0;
  padding: 6px 12px;
  border-radius: var(--r-pill);
  background: var(--accent-bg);
  border: 1px solid oklch(0.55 0.18 143 / 0.4);
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
}
.tutoriales-pregunta__resuelta .fa-check-circle { font-size: 14px; }

/* Mismo punto de corte que el resto del archivo. En vertical la tarjeta gana
   el ancho que le quitaba el relleno lateral. */
@media (max-width: 640px) {
  .tutoriales-guia { padding: 14px 14px; }
  .tutoriales-preguntas__titulo { font-size: 18px; }
  .tutoriales-pregunta__enviar { flex: 1 1 100%; }
}

/* ══════ MARCADORES (/LEADERBOARDS) — INCLUYE LA VARIANTE TEAMONLY ══════ */
/* ══════════════════════════════════════════════════════════════════════════
   MARCADORES  (/leaderboards, /leaderboards con team_only_leaderboards)
   ══════════════════════════════════════════════════════════════════════════
   Las filas de estos marcadores las visten dos hojas heredadas —scores.css y
   material.css— escritas para el tema claro original de echoCTF. Aquí no se
   reescribe aquel marcado (lo comparten once tarjetas y cuatro plantillas de
   fila), se corrige lo que en una consola oscura está mal:

     · `font-family: orbitron`, SIN FAMILIA DE RESPALDO, en el puesto, el nombre
       y la cifra. El proyecto no trae Orbitron: no hay @font-face, no hay
       archivo de fuente en el repositorio y la petición de webfonts del layout
       solo pide Geist, Geist Mono y Material Icons. Medido en el navegador con
       canvas: measureText de «alumnaux 1210» a 16px da 99.09px en `orbitron`,
       99.09px en `serif` y 99.09px con un nombre de fuente inventado — es decir
       las once listas se estaban pintando en la SERIF por omisión del
       navegador, mientras el resto de la consola va en Geist. Se reparte como
       en el resto del sistema: texto en --sans, cifras en --mono.

     · `.leader-name { white-space: nowrap }` sin `min-width: 0`: un elemento
       flexible no encoge por debajo de su contenido, así que el nombre empujaba
       la CIFRA fuera de la tarjeta. Medido a 375px de ancho de ventana: el
       contenedor medía 305px y el borde derecho del tiempo caía a 400px, o sea
       que en un teléfono se veían los nombres y había que arrastrar cada lista
       de lado para ver los resultados. En un marcador el número es el dato.

     · 4px DE SEPARADOR ENTRE FILAS, EN DOS GRISES DISTINTOS. Cada fila trae un
       `<div class="border">` arriba y otro abajo, y `border` es además una
       clase de utilidad de Bootstrap. Las dos definiciones se acumulan sobre el
       mismo elemento: `background: #aaaaaa` (scores.css) MÁS
       `border: 1px solid #dee2e6` (Bootstrap), 2px de alto cada uno. Entre dos
       filas consecutivas coinciden el de abajo de una y el de arriba de la
       siguiente: 4px medidos, en gris claro, sobre un fondo casi negro. Se
       sustituyen por una sola línea del color del sistema.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Grupos de tarjetas ─────────────────────────────────────────────────── */
/* Rótulo corto en monoespaciada con una regleta que llega hasta el borde: es
   un separador de sección de consola, no un titular. Sustituye a los
   «Rankings de <code>mayor cantidad</code>», que resaltaban una palabra suelta
   de la frase en caja de código sin que hubiera código alguno. */
.marcadores-grupo { margin: 0 0 6px; }
.marcadores-grupo-titulo {
  display: flex; align-items: center; gap: 12px;
  margin: 0 0 14px;
  font-family: var(--mono);
  font-size: 11px; font-weight: 600; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--fg-faint);
}
.marcadores-grupo-titulo::after {
  content: ""; flex: 1 1 auto; height: 1px; background: var(--line-soft);
}

/* Las tarjetas de una misma fila terminan a la misma altura aunque una tenga
   diez posiciones y la de al lado esté vacía. La cadena de `flex: 1 1 auto`
   llega hasta la lista porque cada eslabón —columna, tarjeta, envoltorio pjax,
   contenedor del ListView— es un contenedor flexible en columna: si se rompe en
   uno solo, el alto deja de propagarse y la tarjeta corta se queda corta. */
.marcadores-grupo .row > div { display: flex; margin-bottom: 20px; }
.marcadores-tarjeta {
  width: 100%; margin-bottom: 0;
  flex: 1 1 auto;
  display: flex; flex-direction: column;
}
.marcadores-envoltorio,
.marcadores-tarjeta > .card-contenido,
.marcadores-envoltorio > .card-contenido {
  flex: 1 1 auto; display: flex; flex-direction: column;
}

/* La lista se desplaza en SU PROPIO contenedor si algo no cabe; el <body>
   nunca. Antes esto lo daba `.table-responsive`, que además declara
   `display: block` sobre un elemento que ya lo es y anuncia una tabla que aquí
   no existe: estas listas son <div>. */
.marcadores-lista {
  flex: 1 1 auto;
  overflow-x: auto;
  padding: 2px 18px 12px;
}

/* ── Fila de marcador ───────────────────────────────────────────────────── */
.scoreboard-index .leader { padding: 0; }
.scoreboard-index .leader-wrap {
  display: flex; align-items: center; gap: 10px;
  margin: 0; padding: 9px 0;
}

/* Una sola línea entre filas, y del color del sistema. Se apagan los dos
   `<div class="border">` de cada fila (ver la cabecera de esta sección) y la
   separación la da el borde superior de la fila siguiente, que por definición
   no puede duplicarse ni aparecer suelto al principio o al final de la lista. */
.scoreboard-index .leader > .border { display: none; }
.scoreboard-index .leader + .leader { border-top: 1px solid var(--line-soft); }

.scoreboard-index .leader-place {
  flex: 0 0 auto; min-width: 2.9em; padding-right: 0;
  font-family: var(--mono); font-size: 11.5px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--fg-faint);
}

.scoreboard-index .leader-ava { flex: 0 0 auto; padding: 0; margin: 0; }
.scoreboard-index .leader-ava img {
  display: block; width: 30px; height: 30px;
  object-fit: cover; border-radius: 6px;
}
/* El triangulito blanco bajo el avatar (`.leader-ava::after`, borde de 20px a
   cada lado en #fff) es un adorno del tema claro: sobre fondo oscuro es una
   raya blanca suelta que no significa nada. La medalla del podio ya la marca
   `.rankpos-1/2/3` en el borde de la imagen, y el puesto está escrito al lado
   en texto, que es lo que exige no depender solo del color. */
.scoreboard-index .leader-ava::after { content: none; }

.scoreboard-index .leader-name {
  flex: 1 1 auto; min-width: 0; margin-left: 0;
  font-family: var(--sans); font-size: 13.5px; letter-spacing: normal;
  color: var(--fg);
  overflow: hidden; text-overflow: ellipsis;
}

/* `width: auto` con !important, y es la excepción: `_most_writeup.php` y
   `_team_score.php` traen `style="width: 100%"` ESCRITO EN EL ATRIBUTO, y un
   estilo en línea gana a cualquier selector sin !important. Con dos elementos
   flexibles al 100% la cifra se llevaba la mitad del renglón y el nombre se
   quedaba sin sitio. Las plantillas que sí se han tocado ya no lo llevan. */
.scoreboard-index .leader-score_title {
  flex: 0 0 auto; width: auto !important; text-align: right;
  font-family: var(--mono); font-size: 13px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--fg);
  white-space: nowrap;
}

/* Segundo renglón: en qué máquina o en qué reto. Antes iba pegado al nombre en
   la misma línea sin corte y era lo que empujaba la cifra fuera de la tarjeta.
   Mismo tratamiento que `.target-meta` en el listado de máquinas: dato
   secundario, más pequeño y en tono apagado. */
.marcadores-quien { display: block; overflow: hidden; text-overflow: ellipsis; }
.marcadores-donde {
  display: block; margin-top: 1px;
  font-size: 11px; color: var(--fg-mute);
  overflow: hidden; text-overflow: ellipsis;
}
.marcadores-donde a { color: var(--fg-dim); }
.marcadores-donde a:hover { color: var(--accent); text-decoration: none; }

/* La barra de proporción, con el acento del sistema en vez del lima #94c11f
   del tema anterior. Solo se pinta cuando hay una escala contra la que
   comparar: las plantillas ya no la emiten cuando la cifra es un tiempo.

   Y CON CARRIL. La escala de estas barras es el total de puntos de la
   plataforma, así que los valores reales son fracciones pequeñas: 1.210 puntos
   sobre el total daban una barra de 14px suelta en mitad de la nada, que a
   simple vista parece una coma perdida, no una medida. Sobre un carril se lee
   lo que es —una parte de un todo— y además se ve cuánto queda. */
.scoreboard-index .leader-bar {
  margin: 0 0 9px;
  background: var(--bg-3);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.scoreboard-index .leader-bar .bar {
  height: 3px; border-radius: var(--r-pill);
  background: var(--accent-dim);
}

/* ── Estado vacío dentro de una tarjeta ─────────────────────────────────── */
/* Reutiliza `.estado-vacio-bloque` —el mismo recuadro de trazo discontinuo que
   el listado de equipos— con menos relleno, porque aquí vive dentro de una
   tarjeta estrecha y no a lo ancho de la página. */
.marcadores-vacio { padding: 20px 16px; font-size: 13px; }

/* ── Pie con el paginador ───────────────────────────────────────────────── */
.marcadores-pie { display: flex; justify-content: center; padding: 8px 18px 12px; }
.marcadores-pie .pagination { margin: 0; }

@media (max-width: 640px) {
  .marcadores-grupo .row > div { margin-bottom: 14px; }
  .marcadores-lista { padding-left: 14px; padding-right: 14px; }
  .scoreboard-index .leader-place { min-width: 2.5em; font-size: 11px; }
  .scoreboard-index .leader-name { font-size: 13px; }
}

/* ══════ REDES (/NETWORKS Y /NETWORK/N) ══════ */
/* ══════════════════════════════════════════════════════════════════════════
   REDES  —  /networks (listado) y /network/N (ficha)
   ══════════════════════════════════════════════════════════════════════════
   La tarjeta de red venia del tema heredado: `card-profile`, `card-icon
   bg-primary`, `badge badge-primary`, `orbitron` y `btn-danger` para invitar a
   suscribirse. Cinco decisiones de color y tipografia que no eran de este
   sistema, y ninguna de ellas contestaba a lo unico que se le pregunta a un
   listado de redes: que hay dentro y si puedo entrar.

   Aqui el peso visual esta repartido en ese orden: nombre, cuantas maquinas y
   estado de acceso arriba; la lista de maquinas —que en este despliegue es el
   unico sitio de la aplicacion donde aparecen, porque `on_network = 1` las
   saca del listado general— en el cuerpo; y una sola accion en el pie. */

/* Frase de contexto de la pantalla. Mismas medidas que `.equipos-intro`, que
   cumple el mismo papel en Equipos; no se comparte selector para que tocar una
   no mueva la otra. */
.redes-intro { color: var(--fg-mute); font-size: 14px; max-width: 68ch; margin: 0 0 18px; }
.redes-lista { margin-top: 4px; }

/* El estado vacio lo envuelve ListView en `<div class="empty">` y lo cuelga
   DIRECTAMENTE de `.row`, sin columna. `.row` es flex y lleva margenes
   negativos de 15px, asi que ese div se quedaba estrecho y sobresaliendo por la
   izquierda. Comprobado con la unica red del despliegue desactivada. */
.redes-lista > .empty { flex: 1 1 100%; margin: 0 15px; }

/* ---------- Tarjeta ---------- */
/* La columna la estira (`d-flex align-items-stretch`); la tarjeta se reparte
   por dentro para que TODOS los pies queden a la misma altura aunque una red
   tenga tres lineas de descripcion y la de al lado ninguna. */
.redes-tarjeta { width: 100%; display: flex; flex-direction: column; }
.redes-cuerpo { flex: 1 1 auto; }

.redes-cab { display: flex; align-items: flex-start; gap: 12px; }
.redes-cab-txt { min-width: 0; }

/* El nombre de la red, en la tipografia de texto y no en la monoespaciada
   ancha de `.card-title`: es un nombre propio que se lee, no una etiqueta de
   seccion. `min-width: 0` arriba mas `overflow-wrap` aqui para que un nombre
   sin espacios parta en vez de estirar la tarjeta. */
.redes-nombre {
  margin: 0; font-size: 17px; font-weight: 600; line-height: 1.25;
  color: var(--fg); overflow-wrap: anywhere;
}
.redes-nombre a { color: inherit; }
.redes-nombre a:hover { color: var(--accent); text-decoration: none; }
.redes-nombre a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

.redes-datos {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin: 6px 0 0; font-size: 12px;
}
.redes-cuenta { color: var(--fg-mute); font-family: var(--mono); }

/* Estado de acceso. El punto de color es un adorno para poder barrer la
   rejilla de un vistazo: la palabra («Abierta», «Con acceso», «Sin acceso»)
   esta siempre escrita al lado, asi que el color nunca es el unico indicador
   —ni para quien no lo distingue ni en una impresion en blanco y negro. */
.redes-acceso {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: var(--r-pill);
  font-size: 11px; font-weight: 600; letter-spacing: .03em; white-space: nowrap;
}
.redes-acceso::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.redes-acceso--abierta { background: var(--accent-bg); border: 1px solid oklch(0.55 0.18 143 / 0.4); color: var(--accent); }
.redes-acceso--cerrada { background: var(--bg-2); border: 1px solid var(--line); color: var(--fg-mute); }

.redes-desc { margin: 12px 0 0; color: var(--fg-dim); font-size: 13px; line-height: 1.55; }

/* ---------- Maquinas de la red ---------- */
.redes-maquinas { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.redes-maquinas-rotulo {
  margin: 0 0 6px;
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  letter-spacing: .22em; text-transform: uppercase; color: var(--fg-faint);
}
.redes-maquinas-lista { list-style: none; margin: 0; padding: 0; }
.redes-maquinas-lista li {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 5px 0; border-bottom: 1px solid var(--line-soft);
}
.redes-maquinas-lista li:last-child { border-bottom: 0; }
.redes-maquina-nombre { font-family: var(--mono); font-size: 12.5px; color: var(--fg-dim); overflow-wrap: anywhere; }
a.redes-maquina-nombre:hover { color: var(--accent); text-decoration: none; }
a.redes-maquina-nombre:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
/* El color lo pone `.meta-nivel.nivel-N`, la misma escala que ya usa el
   listado de Maquinas: el mismo nivel se pinta igual en las dos pantallas. */
.redes-maquina-nivel { font-size: 11px; letter-spacing: .04em; white-space: nowrap; flex: none; }

.redes-maquinas-mas { margin: 8px 0 0; color: var(--fg-mute); font-size: 12px; }
.redes-maquinas-vacio { margin: 14px 0 0; color: var(--fg-mute); font-size: 12.5px; }

/* ---------- Pie de la tarjeta ---------- */
.redes-pie { border-top: 1px solid var(--line-soft); }
.redes-btn { width: 100%; justify-content: center; }
/* Cuando no hay accion que ofrecer, el pie explica por que en vez de quedarse
   vacio: una tarjeta sin boton se lee como una tarjeta a la que le falta algo. */
.redes-nota { margin: 0; color: var(--fg-mute); font-size: 12.5px; line-height: 1.5; }

/* ══════════════════════════════════════════════════════════════════════════
   FICHA DE UNA RED  (/network/N)
   ══════════════════════════════════════════════════════════════════════════ */

/* No tener acceso a una red privada no es un error del alumno ni de la
   plataforma: es como esta configurada la red. Ambar de aviso, no rojo de
   fallo, y sin `role="alert"` —que es para mensajes que aparecen sobre la
   marcha, no para contenido que ya esta ahi al cargar. */
.redes-aviso {
  margin: 0 0 18px; padding: 12px 14px;
  background: var(--warn-bg); border: 1px solid oklch(0.60 0.14 80 / 0.45);
  border-radius: var(--r-sm);
  color: var(--fg-dim); font-size: 13px; line-height: 1.55;
}
.redes-aviso strong { color: var(--warn); }

/* Descripcion de la red: un panel corto encima de la tabla, no una tarjeta en
   una columna lateral que repetia el nombre y el contador que ya estan en el
   encabezado de la pagina. */
.redes-ficha {
  display: flex; align-items: flex-start; gap: 14px;
  margin: 0 0 18px; padding: 14px 16px;
  background: var(--bg-1); border: 1px solid var(--line-soft); border-radius: var(--r-md);
}
.redes-ficha-texto { min-width: 0; color: var(--fg-dim); font-size: 13.5px; line-height: 1.6; }
.redes-ficha-texto > :first-child { margin-top: 0; }
.redes-ficha-texto > :last-child { margin-bottom: 0; }

/* 640 es el corte que ya usa el resto del archivo. Por debajo, el icono encima
   del texto: 56px de miniatura mas una descripcion dejan menos de 20 caracteres
   por linea en un movil estrecho. */
@media (max-width: 640px) {
  .redes-ficha { flex-direction: column; gap: 10px; }
  .redes-maquinas-lista li { flex-wrap: wrap; gap: 2px 12px; }
}
/* ── CORRECCIONES DE LA REVISIÓN DE DISEÑO ────────────────────────────────── */

/* BLOQUEANTE 2 — el segundo renglón de una fila de marcador se recortaba.
 *
 * `material.css:994` declara `.scoreboard-index .leader-name { white-space:
 * nowrap }`. El bloque nuevo de Marcadores pisa `font-family` y
 * `letter-spacing` de esa regla, pero NO `white-space`, así que `.marcadores-
 * donde` —el renglón de debajo, con el nombre de la máquina o del reto— lo
 * heredaba. Medido a 360 px: «en Trivia de fundamentos» ocupaba 127 px en 124
 * disponibles y salía elidido.
 *
 * Es justo lo que la plantilla decía haber resuelto al bajar la máquina a un
 * segundo renglón: se bajaba, y se cortaba igual.
 *
 * El nombre de la persona sí sigue sin partirse —es corto y partirlo se lee
 * peor—; lo que se libera es la línea de contexto. */
.marcadores-quien { white-space: nowrap; }
.marcadores-donde { white-space: normal; }

/* BLOQUEANTE 3 — foco visible en las tarjetas de reto SIN depender de :has().
 *
 * El bloque de Retos apaga el contorno del enlace y confía el foco entero a
 * `.retos-tarjeta:has(.retos-nombre a:focus-visible)`. En un navegador sin
 * `:has()` eso deja las seis tarjetas sin ningún indicador de foco: quien
 * navega con teclado no sabe dónde está.
 *
 * La vista de Tutoriales sí puso esta red de seguridad; Retos se quedó sin
 * ella. Dos agentes, el mismo patrón, una con paracaídas y otra sin él. */
@supports not selector(:has(a)) {
  .retos-nombre a:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--r-sm);
  }
}

/* El carril de marcadores, con el mismo criterio que el de filtros de máquinas:
 * `overflow-x: auto` hace que `overflow-y` compute a `auto` y puede sacar una
 * barra vertical parásita; y 2 px de relleno no dan para los 4 que ocupa un
 * contorno de foco con su desplazamiento. */
.marcadores-lista {
  overflow-y: hidden;
  padding-top: 4px;
}

/* Estado «apagada» donde antes iba una IP falsa (0.0.0.0). No es un enlace ni
 * se puede copiar, y se distingue de una direccion real por tipografia y color:
 * lo que hay ahi no es un dato, es la ausencia de uno. */
.target-apagada {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .04em;
  color: var(--fg-mute);
  opacity: .85;
}

/* ══════════════════════════════════════════════════════════════════════════
   PANEL DE LA MÁQUINA ENCENDIDA  (dashboard, fase 5)
   ══════════════════════════════════════════════════════════════════════════
   Reutiliza el mismo lenguaje que el resto del tema —gradiente de tarjeta,
   líneas, tokens de color, mono para los datos— para no introducir un quinto
   estilo de tarjeta. Los colores de estado y de nivel salen de los tokens ya
   definidos (--accent, --info, --warn, .meta-nivel), así que un mismo nivel se
   pinta igual aquí que en Máquinas y en Redes.
   ══════════════════════════════════════════════════════════════════════════ */
.instancia { display: block; margin: 0 0 22px; }

/* Sin instancia: una barra discreta, no una tarjeta vacía que parezca rota. */
.instancia-vacia {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px;
  padding: 12px 16px;
  background: var(--bg-1); border: 1px dashed var(--line-soft); border-radius: var(--r-md);
}
.instancia-vacia-txt { color: var(--fg-mute); font-size: 13.5px; }
.instancia-vacia-enlace { color: var(--accent); font-size: 13.5px; font-weight: 600; }
.instancia-vacia-enlace:hover { color: var(--accent-1); text-decoration: none; }
.instancia-vacia-enlace:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

/* La tarjeta. Un filo de acento a la izquierda para que «tu máquina» destaque
   de las tarjetas de estadística que van debajo, sin gritar. */
.instancia-tarjeta {
  background: var(--card-grad);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  padding: 16px 18px;
}

.instancia-cab { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.instancia-rotulo {
  margin: 0;
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  letter-spacing: .22em; text-transform: uppercase; color: var(--fg-faint);
}

/* Pastilla de estado. El color acompaña; la palabra siempre está escrita, así
   que nunca es el único indicador (igual criterio que .redes-acceso). */
.instancia-estado {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: var(--r-pill);
  font-size: 11px; font-weight: 600; letter-spacing: .03em; white-space: nowrap;
}
.instancia-estado::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.instancia-estado--viva { background: var(--accent-bg); border: 1px solid oklch(0.55 0.18 143 / 0.4); color: var(--accent); }
.instancia-estado--iniciando { background: var(--info-bg); border: 1px solid oklch(0.55 0.14 225 / 0.4); color: var(--info); }
.instancia-estado--apagando { background: var(--warn-bg); border: 1px solid oklch(0.60 0.14 80 / 0.45); color: var(--warn); }

.instancia-cuerpo { margin-top: 12px; }

.instancia-identidad { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; }
.instancia-nombre {
  font-size: 20px; font-weight: 600; line-height: 1.2; color: var(--fg); overflow-wrap: anywhere;
}
.instancia-nombre:hover { color: var(--accent); text-decoration: none; }
.instancia-nombre:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.instancia-meta { display: inline-flex; align-items: baseline; gap: 8px; font-size: 12.5px; }
.instancia-nivel { font-weight: 600; }
.instancia-sep { color: var(--fg-faint); }
.instancia-categoria { color: var(--fg-mute); }

/* IP y reloj, uno al lado del otro; envuelven en móvil. */
.instancia-datos { display: flex; flex-wrap: wrap; gap: 10px 28px; margin-top: 14px; }
.instancia-dato { display: flex; flex-direction: column; gap: 4px; }
.instancia-dato-rotulo {
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--fg-faint);
}
.instancia-ip-caja { display: inline-flex; align-items: center; gap: 6px; }
.instancia-ip {
  font-family: var(--mono); font-size: 18px; color: var(--fg);
  background: var(--bg-2); border: 1px solid var(--line-soft);
  border-radius: var(--r-sm); padding: 3px 10px; letter-spacing: .02em;
}
.instancia-copiar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0;
  background: transparent; border: 1px solid var(--line-soft); border-radius: var(--r-sm);
  color: var(--fg-mute); cursor: pointer; transition: color .15s var(--t), border-color .15s var(--t);
}
.instancia-copiar:hover { color: var(--accent); border-color: var(--line-bright); }
.instancia-copiar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.instancia-copiar.copiada { color: var(--accent); border-color: var(--accent); }
.instancia-copiar.copiada::after { content: "✓"; }
/* Fallo al copiar (sin portapapeles y sin execCommand): se marca en rojo y el
   panel muestra un aviso; el botón no debe quedarse callado como si hubiera
   copiado. */
.instancia-copiar.copia-fallo { color: var(--danger); border-color: oklch(0.55 0.18 20 / 0.5); }

/* El reloj: cifras tabulares para que no «baile» al cambiar cada segundo. */
.instancia-reloj {
  font-family: var(--mono); font-size: 18px; color: var(--fg);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
  letter-spacing: .04em;
}
.instancia-nota { margin: 12px 0 0; color: var(--fg-mute); font-size: 12px; line-height: 1.5; }
.instancia-aviso { margin: 12px 0 0; color: var(--fg-dim); font-size: 13px; line-height: 1.55; }
/* Aviso que inyecta el JS (role="alert") cuando algo falla de veras: apagado no
   programado, copiado imposible. Rojo tenue para distinguirse de la nota gris. */
.instancia-aviso-js {
  margin: 12px 0 0; padding: 8px 12px;
  background: var(--danger-bg); border: 1px solid oklch(0.55 0.18 20 / 0.4);
  border-radius: var(--r-sm); color: var(--danger); font-size: 13px; line-height: 1.5;
}

/* Pie con las acciones. */
.instancia-pie { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 16px; }
.instancia-apagar-form { margin: 0; }
.instancia-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 8px 18px; border-radius: var(--r-sm);
  font-family: var(--sans); font-size: 13.5px; font-weight: 600; line-height: 1;
  border: 1px solid transparent; cursor: pointer; text-decoration: none;
  transition: background .15s var(--t), border-color .15s var(--t), color .15s var(--t);
}
.instancia-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.instancia-btn--apagar { background: var(--danger-bg); border-color: oklch(0.55 0.18 20 / 0.5); color: var(--danger); }
.instancia-btn--apagar:hover { background: oklch(0.55 0.18 20 / 0.22); border-color: var(--danger); }
.instancia-btn--apagar.is-cargando { opacity: .6; cursor: progress; }
.instancia-btn--ver { background: var(--bg-2); border-color: var(--line); color: var(--fg-dim); }
.instancia-btn--ver:hover { background: var(--bg-3); border-color: var(--line-bright); color: var(--fg); }
/* «Cancelar» un arranque no es destructivo como «Apagar»: estilo neutro (no
   rojo) para no frenar de más a quien solo se arrepintió de encender. */
.instancia-btn--cancelar { background: var(--bg-2); border-color: var(--line); color: var(--fg-dim); }
.instancia-btn--cancelar:hover { background: var(--bg-3); border-color: var(--line-bright); color: var(--fg); }
.instancia-btn--cancelar.is-cargando { opacity: .6; cursor: progress; }

@media (max-width: 640px) {
  .instancia-datos { gap: 12px 20px; }
  .instancia-pie .instancia-btn { flex: 1 1 auto; }
}
