/* SHARED ORIGIN: solojuntos/src/static/app/index.html (body background
   gradient) — see teamdocs/knowledge/REGISTRO_CODIGO_COMPARTIDO.md.
   The gradient below is the shared Solo-family background (decided
   2026-08-02, Ramón). --fuego/--oro are SoloInsulto's OWN personality
   accent on top of it, per
   teamdocs/knowledge/DISENO_PERSONALIDAD_FAMILIA_SOLO.md — used
   selectively (buttons, chips, highlights, some titles), not as a
   full-palette swap.

   Merged from two previously-duplicated inline <style> blocks (app
   shell + landing page). Where the two disagreed on the same class
   name, the landing-specific version is scoped under `.wrap` (its own
   existing container, present only on the landing page) so both pages
   keep their original appearance untouched except the shared
   background. */
/* SHARED ORIGIN: solo2/web-landing/fonts/ (IBM Plex, SIL Open Font
   License, self-hosted) — see teamdocs/knowledge/REGISTRO_CODIGO_
   COMPARTIDO.md. Family-wide typography decision, 2026-08-15, Ramón —
   see teamdocs/knowledge/DISENO_PERSONALIDAD_FAMILIA_SOLO.md for the
   full rationale (why Plex over Inter, why the Latin subset, why the
   fallback tail after 'Noto Sans' must never be trimmed). */
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:100 700;
  font-display:swap;src:url('/fonts/ibm-plex-sans-latin-var.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;
  font-display:swap;src:url('/fonts/ibm-plex-mono-latin-400.woff2') format('woff2')}
:root{
  --fondo-familia-1:#1a1040;
  --fondo-familia-2:#0f0a26;
  --fondo-familia-3:#0a0e1a;
  --fondo:#1a1210;
  --fondo-alto:#241a17;
  --tinta:#f2e8e4;
  --tinta-suave:#c9b8b2;
  --tinta-tenue:#8a7570;
  --fuego:#ff5a3c;
  --fuego-hueco:rgba(255,90,60,.12);
  --oro:#f5c542;
  --linea:rgba(255,255,255,.09);
  --vidrio:rgba(255,255,255,.035);
  --error:#ff6b6b;
  --exito:#6bcf8f;
  --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;
  --sans:'IBM Plex Sans',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
}
*{box-sizing:border-box}
[hidden]{display:none !important}
html,body{margin:0;padding:0;height:100%}
body{
  background:radial-gradient(120% 120% at 50% 0%,var(--fondo-familia-1) 0%,var(--fondo-familia-2) 45%,var(--fondo-familia-3) 100%);
  background-attachment:fixed;
  color:var(--tinta);
  font-family:var(--sans);
  min-height:100vh;
}
body.app-chrome{padding-bottom:64px}
main h1,main h2,main h3{font-weight:700;letter-spacing:-.01em}
a{color:var(--fuego)}
.wordmark{font-weight:800;color:var(--fuego);text-decoration:none;font-size:1.2rem}
.wrap{max-width:720px;margin:0 auto;padding:0 20px;line-height:1.6}
.wrap .wordmark{font-size:1.6rem}

/* App shell chrome */
header.app-header{
  display:none;
  align-items:center;justify-content:space-between;
  padding:14px 18px;border-bottom:1px solid var(--linea);
  position:sticky;top:0;background:var(--fondo);z-index:10;
}
body.app-chrome header.app-header{display:flex}
/* #header-greeting swaps "SoloInsulto" for "Hola, {display_name}" once
   logged in (perfil.js) — a display_name has no length cap, so the
   TEXT must truncate rather than push the quota pill/Salir off-screen
   or wrap to a second line on a narrow phone. #header-avatar (own
   photo, 2026-08-06) sits before it, sized fixed by .avatar so only
   the text shrinks. */
header.app-header .wordmark{
  flex:1 1 auto;min-width:0;
  display:flex;align-items:center;gap:.5rem;
}
#header-greeting-text{
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
}
header.app-header .row{flex-shrink:0}
.saldo-pill{
  font-family:var(--mono);font-size:.85rem;
  background:var(--vidrio);border:1px solid var(--linea);
  border-radius:999px;padding:4px 12px;color:var(--oro);
}
main{max-width:640px;margin:0 auto;padding:20px 16px}
/* Table-bearing views (marked .view-ancha, e.g. #view-votar) want the
   width the new sortable table can actually use on a wide desktop
   window, instead of the old phone-first 640px card cap. min(96vw,1400px)
   grows with the viewport but stops at 1400px on ultrawide monitors; on
   a phone 96vw is already narrower than 640px, so the viewport itself
   remains the binding constraint there — narrow-screen layout is
   unchanged. */
main:has(.view.active.view-ancha){
  max-width: min(96vw, 1400px);
}
.view{display:none}
.view.active{display:block}
nav.bottom-nav{
  display:none;position:fixed;bottom:0;left:0;right:0;
  background:var(--fondo);border-top:1px solid var(--linea);
  justify-content:space-around;padding:8px 0;z-index:10;
}
body.app-chrome nav.bottom-nav{display:flex}
nav.bottom-nav button{
  background:none;border:none;color:var(--tinta-suave);
  font-size:.68rem;padding:4px 4px;font-weight:700;
}
nav.bottom-nav button.active{color:var(--fuego)}
.pwa-banner{
  position:fixed;left:12px;right:12px;bottom:76px;z-index:20;
  background:var(--fondo-alto);border:1px solid var(--linea);
  border-radius:12px;padding:12px 14px;display:flex;gap:10px;align-items:center;
  box-shadow:0 8px 24px rgba(0,0,0,.4);
}
.pwa-banner p{margin:0;flex:1;font-size:.85rem}

/* Forms + cards, shared everywhere */
.card{
  background:var(--vidrio);border:1px solid var(--linea);
  border-radius:14px;padding:20px;margin-bottom:16px;
}
label{display:block;margin:14px 0 6px;font-size:.9rem;color:var(--tinta-suave)}
input[type=text],input[type=password],input[type=number],textarea,select{
  width:100%;padding:11px 13px;border-radius:10px;
  border:1px solid var(--linea);background:var(--fondo);
  color:var(--tinta);font-size:1rem;font-family:inherit;
}
textarea{resize:vertical;min-height:80px}
input:focus,textarea:focus,select:focus{outline:2px solid var(--fuego);outline-offset:1px}

/* Password reveal, docked inside the field instead of a separate button
   beside it. SHARED ORIGIN: solojuntos/src/static/app/js/auth.js
   wirePasswordReveal() + its .pw-wrap/.pw-ver CSS — see
   teamdocs/knowledge/REGISTRO_CODIGO_COMPARTIDO.md. */
.pw-wrap{position:relative}
.pw-wrap input{padding-right:4.2rem}
.pw-ver{
  position:absolute;right:.45rem;top:50%;transform:translateY(-50%);
  background:none;border:0;color:var(--tinta-tenue);font:inherit;
  font-size:.78rem;cursor:pointer;padding:.3rem .45rem;line-height:1;
}
.pw-ver:active{color:var(--fuego)}
button, .btn{
  cursor:pointer;border:none;border-radius:10px;
  padding:12px 18px;font-size:1rem;font-weight:600;
  font-family:inherit;
}
.wrap .btn{display:inline-block;padding:13px 26px;font-weight:700;text-decoration:none}
button.primary, .btn.primary{background:var(--fuego);color:#1a0e08}
button.secondary, .btn.secondary{background:var(--vidrio);color:var(--tinta);border:1px solid var(--linea)}
button.ghost, .btn.ghost{background:transparent;color:var(--tinta-suave);text-decoration:underline;padding:6px 0}
button:disabled{opacity:.5;cursor:not-allowed}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}

/* Avatars — user photos AND target images, one renderer (avatar.js) for
   both, on this shell and the public landing page alike. The circle's
   fallback colour is written INLINE by the renderer (derived from the
   name, so it can't live in a stylesheet), which is why none is
   declared here. `.avatar-target` is the one visual difference between
   the two owners — a rounded square instead of a full circle, a cheap
   at-a-glance cue for "this is a personaje, not a person" across every
   list that mixes both (e.g. Explorar). */
.avatar{
  flex:0 0 auto;width:2rem;height:2rem;border-radius:50%;overflow:hidden;
  display:inline-flex;align-items:center;justify-content:center;
  font-size:.9rem;font-weight:700;color:var(--tinta);line-height:1;
  background:var(--vidrio);border:1px solid var(--linea);
  user-select:none;-webkit-user-select:none;
}
.avatar img{width:100%;height:100%;object-fit:cover;display:block}
.avatar-grande{width:4.5rem;height:4.5rem;font-size:1.9rem}
.avatar.avatar-target{border-radius:22%}

/* The photo picker, on Mi perfil and on target creation/detail. The
   avatar IS the button: on a phone a 4.5rem circle/square is a far
   easier target than a text link, and it shows what you're changing
   while you change it. */
.foto-fila{display:flex;align-items:center;gap:.9rem}
.foto-fila .avatar{cursor:pointer}
.foto-fila>div{flex:1 1 auto;min-width:0}
.foto-acciones{display:flex;flex-direction:column;gap:.4rem;align-items:flex-start;min-width:0}
.foto-acciones button.ghost{padding:0;font-size:.8rem}
/* The "Reportar este personaje" link (2026-08-20, moved off the Insultar
   list) sits at the row's far end, never squeezed out by a long target
   name — same truncate-not-push instinct as header.app-header's own
   #header-greeting-text. */
.foto-fila .flag-link{flex-shrink:0;margin-left:auto;align-self:flex-start}

.error-box{
  background:rgba(255,107,107,.12);border:1px solid var(--error);
  color:var(--error);border-radius:10px;padding:10px 14px;margin-top:12px;font-size:.9rem;
}
.wrap .error-box{background:none;border:none;border-radius:0;margin-top:0;padding:16px 0}
.empty-box{color:var(--tinta-tenue);padding:16px 0;text-align:center;font-size:.9rem}
.sugerencias{list-style:none;padding:0;margin:8px 0 0}
.sugerencias li{padding:6px 0;border-bottom:1px solid var(--linea);font-size:.9rem;color:var(--tinta-suave)}
.chip{
  display:inline-block;background:var(--fuego-hueco);color:var(--fuego);
  border-radius:999px;padding:2px 10px;font-size:.75rem;font-weight:700;
}
.mono{font-family:var(--mono)}
.uid-box{
  font-family:var(--mono);font-size:.85rem;background:var(--fondo);
  border:1px dashed var(--linea);border-radius:8px;padding:8px 10px;
  word-break:break-all;color:var(--tinta-suave);
}
.cobertura{font-size:.75rem;color:var(--tinta-tenue);margin-top:4px}
/* Fine-print asides, smaller than .cobertura's own hint size — Reglas y
   ayuda's own clarifying notes (e.g. "esto se retira al instante"),
   never the primary statement of a rule. */
.cobertura-micro{font-size:.65rem;color:var(--tinta-tenue);margin-top:4px}

/* Rankings, shared between the app view and the landing widget */
.rank-item{
  display:flex;align-items:center;gap:12px;padding:10px 0;
  border-bottom:1px solid var(--linea);
}
.rank-item .puesto{
  font-family:var(--mono);color:var(--oro);font-weight:700;width:28px;text-align:right;
}
.rank-item .nombre{flex:1;font-weight:600}
.rank-item .valor{font-family:var(--mono);color:var(--tinta-suave)}

/* Insultos + voting (app only) */
.insulto-card{border-bottom:1px solid var(--linea);padding:14px 0}
.insulto-texto{font-size:1.05rem;line-height:1.5;margin:0 0 8px}
.insulto-meta{display:flex;gap:14px;font-size:.8rem;color:var(--tinta-tenue);align-items:center}
/* Per-category vote chips (insultos.js's crearChipVoto, round 5 — replaces
   the old 1-5×3 .score-row/.score-btn grid, removed). Base shape borrows
   .chip's rounded-pill/font weight but starts muted (not .chip's own
   fuego tint) since a chip here is always visible, not just a highlight —
   the fuego tint below is reserved for "I voted this", so it still reads
   as a single meaning, same as everywhere else in this app. */
.chip-voto{
  cursor:pointer;background:var(--vidrio);color:var(--tinta-suave);
  border:1px solid var(--linea);border-radius:999px;padding:6px 14px;
  font-size:.8rem;font-weight:700;
}
.chip-voto:hover{border-color:var(--fuego)}
.chip-voto.voto-mio{background:var(--fuego-hueco);color:var(--fuego);border-color:rgba(255,90,60,.35)}
.tabs{display:flex;gap:6px;margin-bottom:16px;flex-wrap:wrap}
.tab-btn{background:var(--vidrio);border:1px solid var(--linea);color:var(--tinta-suave);border-radius:999px;padding:8px 14px;font-size:.85rem}
.tab-btn.active{background:var(--fuego);color:#1a0e08;border-color:var(--fuego)}

/* Login/register toggle — a segmented two-way switch (quiet inactive
   text, a tinted-glass active state), not the pill-button .tabs/.tab-btn
   above (kept as-is, shared with the 5-way ranking selector, which is
   a different shape of control). SHARED ORIGIN: solojuntos/src/static/
   app/index.html .auth-tabs/.auth-tab — see REGISTRO_CODIGO_COMPARTIDO.md.
   Color swapped to SoloInsulto's own --fuego accent, not SoloJuntos's gold. */
.auth-tabs{display:flex;gap:8px;margin-bottom:20px}
.auth-tab{
  flex:1;text-align:center;padding:10px;border-radius:10px;cursor:pointer;
  font-size:.9rem;font-weight:600;color:var(--tinta-tenue);
  border:1px solid transparent;background:none;
}
.auth-tab.active{color:var(--fuego);border-color:rgba(255,90,60,.35);background:var(--fuego-hueco)}
.flag-link{color:var(--tinta-tenue);font-size:.78rem;text-decoration:underline;cursor:pointer;background:none;border:none;padding:0;font-family:inherit}

/* Registration depth: honeypot (visually hidden, still bot-fillable —
   never display:none/hidden, some crawlers skip those), password
   strength + username-availability inline feedback */
.hp-field{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}
.fuerza-debil{color:var(--error)}
.fuerza-media{color:var(--oro)}
.fuerza-fuerte{color:var(--exito)}
.disponible{color:var(--exito)}
.no-disponible{color:var(--error)}

/* QR scan modal (scanner.js) + QR display frame (conexiones.js, cont 11).
   SHARED ORIGIN: solojuntos/src/static/app/index.html's .qr-modal/
   .qr-viewport/.qr-quiet-zone inline <style> block — see
   teamdocs/knowledge/REGISTRO_CODIGO_COMPARTIDO.md. Structural rules only;
   card chrome/button/input styling reused from .card/button.secondary/
   input[type=text] above rather than re-declared. */
.qr-modal{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto}
.qr-modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.82)}
.qr-modal-card{position:relative;z-index:1;max-width:360px;width:100%}
.qr-viewport{position:relative;width:100%;aspect-ratio:1/1;border-radius:10px;overflow:hidden;background:#000;margin-bottom:14px}
.qr-viewport video{width:100%;height:100%;object-fit:cover}
.qr-quiet-zone{display:inline-block;background:#fff;padding:20px;border-radius:10px;line-height:0}
.qr-quiet-zone canvas{display:block}

/* Landing page only */
header.hero{padding:64px 0 40px;text-align:center}
h1{font-size:2rem;font-weight:800;letter-spacing:-.02em;margin:20px 0 12px}
.pitch{font-size:1.1rem;color:var(--tinta-suave);max-width:560px;margin:0 auto}
.pitch strong{color:var(--tinta)}
.cta-row{margin-top:28px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
/* :not(.view) — this rule is landing-only (see comment above), but the
   bare element selector was also matching every app-shell .view section
   (same <section> tag), stacking a second 36px+divider on top of main's
   own padding and the header's own border-bottom. Confirmed live via
   getComputedStyle before fixing, not assumed. */
section:not(.view){padding:36px 0;border-top:1px solid var(--linea)}
h2{font-size:1.3rem;font-weight:700;margin-bottom:6px}
.definicion{
  font-style:italic;color:var(--tinta-suave);border-left:3px solid var(--fuego);
  padding-left:16px;margin:16px 0;
}
.contraste{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:20px}
@media (max-width:520px){.contraste{grid-template-columns:1fr}}
.contraste .card{background:var(--vidrio);border:1px solid var(--linea);border-radius:12px;padding:18px;margin-bottom:0}
.contraste .card h3{margin:0 0 8px;font-size:.95rem;color:var(--tinta-tenue);text-transform:uppercase;letter-spacing:.04em}
.contraste .card.malo{border-color:rgba(255,107,107,.25)}
.contraste .card.bueno{border-color:var(--fuego)}
footer{padding:32px 0;text-align:center;color:var(--tinta-tenue);font-size:.85rem}
footer a{color:var(--tinta-suave)}

/* Generic sortable/scrollable table component (tabla.js) — reusable
   across every list screen (Votar pilot first, more screens later). */
.tabla-scroll{
  max-height:70vh;overflow-y:auto;overflow-x:auto;
  border:1px solid var(--linea);border-radius:14px;
  background:var(--vidrio);
}
/* Infinite-scroll sentinel (2026-08-05) — zero height when idle, so it
   never shows as empty space; only "Cargando…" while a page fetch is
   in flight (see tabla.js's setCargando). */
.tabla-sentinel{
  text-align:center;color:var(--tinta-tenue);font-size:.8rem;
  padding:0;min-height:0;
}
.tabla-sentinel:not(:empty){padding:10px 0}
/* Same idea, for the plain (non-.tabla) paginated lists — targets,
   explorar, admin, insultos, usuario. */
.scroll-sentinel{
  text-align:center;color:var(--tinta-tenue);font-size:.8rem;
  padding:0;min-height:0;
}
.scroll-sentinel:not(:empty){padding:10px 0}
.tabla{width:100%;border-collapse:collapse;font-size:.9rem}
.tabla thead th{
  position:sticky;top:0;background:var(--fondo);
  color:var(--tinta-suave);font-weight:700;font-size:.82rem;
  padding:10px 12px;border-bottom:1px solid var(--linea);
  white-space:nowrap;text-align:left;
}
.tabla th.tabla-th-sortable{cursor:pointer;user-select:none}
.tabla th.tabla-th-sortable:hover{color:var(--tinta)}
.tabla tbody tr{border-bottom:1px solid var(--linea)}
.tabla tbody tr:last-child{border-bottom:none}
.tabla-clickable tbody tr.tabla-row-clickable{cursor:pointer}
.tabla-clickable tbody tr.tabla-row-clickable:hover{background:var(--fuego-hueco)}
.tabla td{padding:10px 12px;text-align:left;line-height:1.4}
/* opts.textoVacio (tabla.js) — a filter matching nothing, not "no data
   at all" (that's the caller's own empty-box, shown instead of ever
   building the table). */
.tabla-td-vacio{text-align:center;color:var(--tinta-tenue);padding:24px 12px}
/* A column's `avatar(row)` config (tabla.js) — the avatar sits inline
   before the cell's own text, small enough not to blow out row height.
   The flex layout lives on an INNER wrapper, never the <td> itself
   (2026-08-20 fix) — `display:flex` directly on a table-cell computes
   its outer display to `block` in Chrome, so it stops participating in
   table column layout entirely. With two such cells in the same row
   (any table with 2+ avatar columns — Votar's Personaje+Autor had this
   too, just never diagnosed) both collapse onto the SAME rect instead
   of sitting side by side, and every later column silently shifts one
   slot left of its own header. */
.tabla td.tabla-td-avatar{padding:0}
.tabla-td-avatar-inner{display:flex;align-items:center;gap:.5rem;padding:10px 12px}
.tabla-avatar{width:1.6rem;height:1.6rem;font-size:.7rem;flex-shrink:0}
/* Column-level `muted: true` (2026-08-20) — a secondary avatar column
   (e.g. Creador, next to a target's own Personaje) reads smaller and
   dimmer, so the primary column keeps the visual weight. */
.tabla-td-avatar-inner-muted{color:var(--tinta-suave)}
.tabla-td-avatar-inner-muted .tabla-avatar{width:1.3rem;height:1.3rem;font-size:.6rem}
/* opts.filaTexto (tabla.js) splits a record into a full-width text row
   plus a compact meta row. Grouping is conveyed by color contrast alone,
   not a divider — neither physical row shows a border-bottom. Both
   overrides below are written as `.tabla tbody tr.tabla-fila-*` (not a
   bare class) so their specificity (0,2,2) actually beats the generic
   `.tabla tbody tr` rule's own (0,1,2) — a bare `.tabla-fila-texto{...}`
   would tie on class-count and then LOSE on element-count, so the border
   would still show through. Font-size/line-height on the text row match
   .insulto-texto, the app's existing typography for this same kind of
   content. */
.tabla tbody tr.tabla-fila-texto{border-bottom:none}
.tabla-fila-texto td{padding:12px 12px 4px;font-size:1.05rem;line-height:1.5}
/* The 4 rules below (desktop-only visual PAIRING with an actual text row)
   are scoped via the adjacent-sibling combinator, not the bare class —
   tabla.js (2026-09-06) now puts `tabla-fila-meta` on EVERY row, filaTexto
   consumer or not, so its narrow-viewport "record card" rules further
   down (already written against this class) reach a plain table like
   Insultar's/Explorar's own too. A plain row has no preceding
   `.tabla-fila-texto` sibling, so it never matches `+` here and keeps its
   ordinary desktop look untouched; only Votar's actual paired rows still
   get the recessed/dimmed pairing treatment below.

   The meta row reads as supporting/secondary information below the text
   row: a visibly darker, recessed background (black overlay, not the old
   3%-white zebra tint that was confirmed invisible on a real monitor) plus
   dimmed text. This is deliberately a flat rule for the whole row — the
   numeric vote columns (Gracia/Ingenio/Elegancia/Total) get their own
   vote-state coloring (white=not voted / accent=voted, see .voto-mio
   below), which overrides this on those specific <td>s; no per-cell hook
   needed for the rest since nothing else consumes it. Row-hover
   (.tabla-row-clickable:hover above) wins on specificity regardless of
   rule order (3 classes/pseudo vs 1 here), so hover still shows through.

   Bottom border: Ramón's own follow-up after seeing this live — the
   background contrast alone still made it hard to tell where one
   record ends and the next begins. A thin, MUTED-orange line (not the
   full-saturation --fuego already used for "I voted this" a few cells
   to the right, on the SAME row — reusing it here for a structural
   divider would collide with that meaning) reinforces the boundary
   without competing with the vote-state color. Reuses the exact tone
   already established for a similar "warm but not shouting" role
   (.chip-voto.voto-mio's own border-color below). */
.tabla tbody tr.tabla-fila-texto+tr.tabla-fila-meta{background:rgba(0,0,0,.2);border-bottom:1px solid rgba(255,90,60,.35)}
/* Without this, the LAST record's own meta row would keep the new
   divider above (it ties the generic `.tabla tbody tr:last-child`
   rule's own specificity — 0,2,2 vs 0,2,2 — and wins on cascade order
   since it's declared later), leaving a stray line sitting right
   against the table's own bottom edge/rounded corner. One extra class
   (0,3,2) settles it unambiguously regardless of source order. */
.tabla tbody tr.tabla-fila-texto+tr.tabla-fila-meta:last-child{border-bottom:none}
.tabla-fila-texto+.tabla-fila-meta td{color:var(--tinta-suave)}
/* Per-column click override (columna.onClick in tabla.js) — applied to
   any tabla.js <td> with a column-level onClick, meta row or plain row
   alike (filaTexto's own text <td> has no per-column concept). Hover
   changes text color, not background, so it doesn't fight the row's own
   hover background above. */
.tabla-td-clickable{cursor:pointer}
.tabla-td-clickable:hover{color:var(--fuego)}
/* Per-category vote-state coloring (votar.js's columna.cellClass, round
   5) — a voted cell reads as "this is mine", using the SAME --fuego
   accent as every other active/mine indicator in this app (.auth-tab.
   active, nav.bottom-nav button.active, .tab-btn.active) — NOT --oro,
   which this app already reserves for a different meaning (money/rank:
   .saldo-pill, .rank-item .puesto). These columns already get
   .tabla-td-clickable for free (they carry columna.onClick too), so
   hovering an unvoted cell already turns --fuego via that rule; a voted
   cell just stays --fuego regardless of hover — same color either way,
   nothing to fight over. Selector needs the extra `.tabla-fila-meta td`
   prefix (not a bare `.voto-mio`) to out-specificity `.tabla-fila-meta
   td` above (0,1,1) — a bare single-class selector would tie on
   class-count and then LOSE on element-count, the same trap already
   documented for .tabla-fila-texto above. Only votar.js ever sets
   `voto-mio`, so this stays inert on every other tabla.js consumer
   regardless of the sibling-combinator scoping above. */
.tabla tbody tr.tabla-fila-meta td.voto-mio{color:var(--fuego);font-weight:700}

/* Narrow-viewport table layout (Ramón, 2026-08-04): the grid-of-columns
   shape that works on a wide desktop window (main:has(.view-ancha)
   above) reads as cramped on a phone — 6 columns squeezed into ~330px
   forces multi-line wraps in every cell. 640px matches this app's own
   established phone-first cap (see main{max-width:640px} + the
   .view-ancha comment above).

   First attempt (kept ONLY the two body-row <tr>s' own display:flex,
   table/thead/tbody left as real table display) live-verified BROKEN:
   a <tr> whose display is overridden away from table-row while its
   parent <tbody> stays table-row-group has no well-defined containing
   block to resolve width:100%/flex-basis:100% against — measured live
   at 138.9px actual row width against a 330px+ container, cutting
   every flex-basis:100% cell down to a useless sliver (AGENDA
   2026-08-04). The fix that actually measured correct (scrollWidth ==
   clientWidth, zero horizontal overflow, verified against a real
   narrow viewport, not just reasoned about) converts the WHOLE
   table/thead/tbody chain to display:block together — the standard,
   well-supported "responsive table" pattern — so every descendant's
   containing block is unambiguous, ordinary block layout.

   Result: <thead>'s row becomes its own wrapped flex row of plain
   tappable labels (sorting stays live, just no longer column-aligned);
   each body row stacks in normal block flow — filaTexto (insulto text)
   unchanged in spirit, meta row becomes Objetivo/Autor/Total each on
   their own line plus a wrapped row of Gracia/Ingenio/Elegancia pills,
   closer to insultos.js's own crearInsultoCard chip look Ramón pointed
   to as the nicer phone read. tabla.js stays the one reusable sortable
   component throughout — only CSS changed, no JS branching by
   viewport. Column-level `chip:true` (tabla.js) is the only per-column
   hook this needs — a future tabla.js screen opts into the same
   treatment per column, same mechanism as cellClass/onClick already
   work. colSpan on filaTexto's <td> (tabla.js) simply stops mattering
   once its row is a lone block-level cell — harmless, still reads
   full-width. */
@media (max-width:640px){
  .tabla{display:block;width:100%}
  .tabla thead{display:block}
  .tabla tbody{display:block}
  .tabla thead tr{
    display:flex;flex-wrap:wrap;gap:4px 10px;
    padding:8px 12px;border-bottom:1px solid var(--linea);
  }
  /* border-bottom moves to the row above — one full-width underline
     instead of a fragmented one under each wrapped header word. */
  .tabla thead th{
    display:block;position:static;white-space:nowrap;
    padding:2px 0;font-size:.75rem;border-bottom:none;
  }
  .tabla tbody tr{display:block}
  .tabla tbody tr.tabla-fila-texto{border-bottom:none}
  .tabla-fila-texto td{display:block;padding:12px 12px 4px;font-size:1.05rem;line-height:1.5}
  .tabla tbody tr.tabla-fila-meta{
    display:flex;flex-wrap:wrap;align-items:center;
    gap:8px 10px;padding:10px 12px;width:100%;
  }
  /* Overrides tabla.js's own inline style.textAlign="right" on numeric
     columns — inline styles otherwise beat any class selector, and
     right-alignment has no meaning once a cell is a wrapped flex/pill
     item instead of a table column. */
  .tabla-fila-meta td{
    display:block;flex:0 0 auto;padding:0;text-align:left !important;
  }
  /* Plain (non-chip) cells — Objetivo/Autor/Total — each force their
     own full line via flex-basis:100%, same effect as a manual line
     break in a wrapped flex row, no DOM reordering needed. */
  .tabla-fila-meta td:not(.tabla-td-chip){
    flex-basis:100%;padding:2px 0;
    font-size:.78rem;color:var(--tinta-tenue);
  }
  /* Every meta cell's column label, injected from data-label (set by
     tabla.js for every meta <td>) — the desktop table gets this from
     <thead> instead, so this stays scoped to the narrow layout only. */
  .tabla-fila-meta td::before{
    content:attr(data-label) ": ";font-weight:600;color:var(--tinta-suave);
  }
  /* An avatar cell's inner wrapper (app.css's own .tabla-td-avatar-inner,
     unconditionally display:flex — a BLOCK-level box) forces the avatar
     onto its own line below this ::before label instead of sitting next
     to it on the same line. Scoped to the narrow layout only, so the
     desktop table (padding/hover-target sizing depend on the block box
     filling its cell) is untouched. */
  .tabla-fila-meta td.tabla-td-avatar .tabla-td-avatar-inner{
    display:inline-flex;
  }
  /* A PLAIN table's own primary identifying column (e.g. Insultar's own
     Personaje, columna.primary in targets.js) reads as the card's
     headline, not one more dim meta line — a Votar meta row has no such
     need since its filaTexto row above already carries the headline, and
     correctly never sets this flag on Objetivo/Autor. NOT detected
     structurally (an earlier version tried "avatar cell that isn't
     muted," which looked right for Insultar but silently ALSO promoted
     Votar's own Autor column — it has no `muted` flag either, being
     secondary for a completely different reason; caught live comparing
     both screens before shipping). Comes after the generic non-chip rule
     above so its own font-size/color win by source order at equal
     specificity. */
  .tabla-fila-meta td.tabla-td-primary{
    font-size:1rem;font-weight:700;color:var(--tinta);padding:2px 0 6px;
  }
  .tabla-fila-meta td.tabla-td-primary::before{
    content:none;
  }
  /* Chip cells — Gracia/Ingenio/Elegancia — borrow .chip-voto's own
     pill shape (rounded, bordered, padded) so a voted/unvoted category
     reads exactly like insultos.js's detail-card chips; three of them
     wrap together on their own line(s) since they're the only auto-
     width items in this row. */
  .tabla-fila-meta td.tabla-td-chip{
    display:inline-flex;align-items:center;
    background:var(--vidrio);color:var(--tinta-suave);
    border:1px solid var(--linea);border-radius:999px;
    padding:6px 12px;font-size:.8rem;font-weight:700;
  }
  .tabla-fila-meta td.tabla-td-chip::before{color:inherit}
  .tabla-fila-meta td.tabla-td-chip.tabla-td-clickable:hover{
    border-color:var(--fuego);color:var(--fuego);
  }
  /* Voted state — same --fuego tint as .chip-voto.voto-mio, replacing
     the desktop table's own text-only .voto-mio treatment (defined
     above, outside this media query) with a full pill tint to match. */
  .tabla-fila-meta td.tabla-td-chip.voto-mio{
    background:var(--fuego-hueco);color:var(--fuego);
    border-color:rgba(255,90,60,.35);
  }
}
