{"id":3113,"date":"2026-09-22T15:56:36","date_gmt":"2026-09-22T18:56:36","guid":{"rendered":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/?page_id=3113"},"modified":"2026-10-20T17:36:56","modified_gmt":"2026-10-20T20:36:56","slug":"biblioteca","status":"publish","type":"page","link":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/biblioteca\/","title":{"rendered":"Cat\u00e1logo Digital &#8211; Biblioteca"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"es\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Cat\u00e1logo en L\u00ednea &#8211; BILIB<\/title>\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Barlow:wght@500;600;700&#038;family=Barlow+Semi+Condensed:wght@600&#038;display=swap\" rel=\"stylesheet\">\n<style>\n  :root{\n    --azul-ifdc:#22409b;\n    --azul-ifdc-oscuro:#1a3480;\n    --azul-nav:#6d7fd6;\n    --celeste:#8bc7e8;\n    --verde:#9ccfa4;\n    --durazno:#f3bd82;\n    --rojo:#d9483f;\n    --rojo-oscuro:#b3261e;\n    --hueso:#faf9f6;\n    --texto:#23283b;\n    --texto-suave:#5b6072;\n    --borde:#e4e2db;\n  }\n\n  \/* Cuando un bibliotecario inici\u00f3 sesi\u00f3n, el color principal pasa a naranja. *\/\n  body.modo-bibliotecario{\n    --azul-ifdc:#c76b1e;\n    --azul-ifdc-oscuro:#a8551a;\n    --azul-nav:#f0a659;\n  }\n\n  *{ box-sizing:border-box; }\n\n  body{\n    margin:0;\n    background:var(--hueso);\n    color:var(--texto);\n    font-family:Calibri,Candara,\"Segoe UI\",Optima,Arial,sans-serif;\n    line-height:1.45;\n  }\n\n  h1,h2,h3,.marca,.materia-libro{\n    font-family:\"Barlow\",Calibri,sans-serif;\n  }\n\n  \/* ---------- Encabezado ---------- *\/\n  .encabezado{\n    background:linear-gradient(135deg,var(--azul-ifdc) 0%,var(--azul-nav) 100%);\n    color:#fff;\n    padding:28px 20px 60px;\n    text-align:center;\n    position:relative;\n  }\n  .encabezado .rotulo{\n    font-family:\"Barlow Semi Condensed\",sans-serif;\n    font-size:13px;\n    letter-spacing:.04em;\n    color:#dfe4ff;\n    margin:0 0 6px;\n  }\n  .encabezado h1{\n    margin:0;\n    font-size:clamp(24px,4vw,34px);\n    font-weight:700;\n  }\n  .encabezado p{\n    margin:8px auto 0;\n    max-width:520px;\n    color:#e7eaff;\n    font-size:15px;\n  }\n\n  .btn-bibliotecarios{\n    position:absolute;\n    top:16px;\n    right:16px;\n    background:rgba(255,255,255,.14);\n    color:#fff;\n    border:1px solid rgba(255,255,255,.5);\n    border-radius:8px;\n    padding:8px 14px;\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    font-size:13px;\n    cursor:pointer;\n    transition:background .15s ease;\n  }\n  .btn-bibliotecarios:hover{ background:rgba(255,255,255,.26); }\n  .btn-bibliotecarios.activo{ background:var(--verde); border-color:var(--verde); color:#173a1e; }\n\n  \/* ---------- Buscador ---------- *\/\n  .panel-busqueda{\n    max-width:720px;\n    margin:-36px auto 0;\n    padding:0 20px;\n    position:relative;\n    z-index:2;\n  }\n  .caja-busqueda{\n    background:#fff;\n    border-radius:14px;\n    box-shadow:0 10px 28px rgba(34,64,155,.16);\n    padding:10px;\n    display:flex;\n    gap:8px;\n  }\n  .caja-busqueda input{\n    flex:1;\n    border:none;\n    outline:none;\n    padding:12px 14px;\n    font-size:16px;\n    font-family:Calibri,Candara,sans-serif;\n    color:var(--texto);\n    background:transparent;\n  }\n  .caja-busqueda button{\n    background:var(--azul-ifdc);\n    color:#fff;\n    border:none;\n    border-radius:9px;\n    padding:0 22px;\n    font-size:15px;\n    font-weight:600;\n    font-family:\"Barlow\",sans-serif;\n    cursor:pointer;\n    transition:background .15s ease;\n  }\n  .caja-busqueda button:hover{ background:var(--azul-ifdc-oscuro); }\n  .caja-busqueda button:focus-visible,\n  .caja-busqueda input:focus-visible{\n    outline:2px solid var(--azul-nav);\n    outline-offset:2px;\n  }\n\n  .estado-busqueda{\n    max-width:720px;\n    margin:14px auto 0;\n    padding:0 24px;\n    font-size:14px;\n    color:var(--texto-suave);\n    min-height:18px;\n  }\n\n  \/* ---------- Barra de bibliotecario ---------- *\/\n  .barra-bibliotecario{\n    max-width:720px;\n    margin:16px auto 0;\n    padding:0 20px;\n    display:flex;\n    justify-content:space-between;\n    align-items:center;\n    gap:10px;\n  }\n  .barra-bibliotecario[hidden]{ display:none; }\n  .barra-bibliotecario .etiqueta{\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    font-size:14px;\n    color:var(--texto-suave);\n  }\n  .btn-agregar{\n    background:var(--azul-ifdc);\n    color:#fff;\n    border:none;\n    border-radius:9px;\n    padding:10px 18px;\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    font-size:14px;\n    cursor:pointer;\n  }\n  .btn-agregar:hover{ background:#1a3480; }\n\n  \/* ---------- Resultados ---------- *\/\n  .resultados{\n    max-width:720px;\n    margin:14px auto 60px;\n    padding:0 20px;\n    display:flex;\n    flex-direction:column;\n    gap:12px;\n  }\n\n  .libro{\n    background:#fff;\n    border:1px solid var(--borde);\n    border-left:6px solid var(--celeste);\n    border-radius:12px;\n    overflow:hidden;\n    transition:background .15s ease, border-color .15s ease, box-shadow .15s ease;\n  }\n  .libro:nth-of-type(3n+2){ border-left-color:var(--verde); }\n  .libro:nth-of-type(3n){ border-left-color:var(--durazno); }\n\n  .libro:hover{ background:var(--celeste); box-shadow:0 6px 18px rgba(34,64,155,.1); }\n  .libro:nth-of-type(3n+2):hover{ background:var(--verde); }\n  .libro:nth-of-type(3n):hover{ background:var(--durazno); }\n  .libro:hover .libro-titulo{ color:#172a6b; }\n  .libro:hover .libro-autor{ color:var(--texto); }\n  .libro-cabecera:hover,.libro-cabecera:focus,.libro-cabecera:active{\n    background:transparent !important;\n    color:inherit !important;\n  }\n\n  .libro-cabecera{\n    width:100%;\n    background:none;\n    border:none;\n    text-align:left;\n    padding:16px 18px;\n    display:flex;\n    align-items:center;\n    gap:14px;\n    cursor:pointer;\n    font-family:inherit;\n    color:inherit;\n  }\n  .libro-cabecera:focus-visible{\n    outline:2px solid var(--azul-nav);\n    outline-offset:-2px;\n  }\n\n  .libro-titulo-bloque{ flex:1; min-width:0; }\n  .libro-titulo{\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    font-size:16.5px;\n    color:var(--azul-ifdc);\n    margin:0 0 3px;\n  }\n  .libro-autor{\n    font-size:14px;\n    color:var(--texto-suave);\n    margin:0;\n    white-space:nowrap;\n    overflow:hidden;\n    text-overflow:ellipsis;\n  }\n\n  .libro-flecha{\n    flex-shrink:0;\n    width:22px;\n    height:22px;\n    color:var(--texto-suave);\n    transition:transform .2s ease;\n  }\n  .libro-cabecera:hover .libro-flecha{ color:var(--azul-ifdc); }\n\n  \/* ---------- Ficha ampliada ---------- *\/\n  .resultados.modo-detalle{ max-width:860px; }\n  .barra-acciones{\n    display:flex;\n    justify-content:space-between;\n    gap:10px;\n    flex-wrap:wrap;\n  }\n  .barra-acciones .grupo{ display:flex; gap:10px; flex-wrap:wrap; }\n  .btn-volver,.btn-imprimir,.btn-editar{\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    font-size:15px;\n    border:2px solid var(--azul-ifdc);\n    border-radius:9px;\n    padding:10px 18px;\n    cursor:pointer;\n    transition:background .15s ease;\n  }\n  .btn-volver{ background:#fff; color:var(--azul-ifdc); }\n  .btn-volver:hover{ background:#eef1fb; }\n  .btn-imprimir{ background:var(--azul-ifdc); color:#fff; }\n  .btn-imprimir:hover{ background:var(--azul-ifdc-oscuro); }\n  .btn-eliminar{ background:var(--rojo); color:#fff; border-color:var(--rojo); }\n  .btn-eliminar:hover{ background:var(--rojo-oscuro); border-color:var(--rojo-oscuro); }\n  .btn-editar{ background:var(--durazno); color:#6b3d0a; border-color:var(--durazno); }\n  .btn-editar:hover{ background:#eda85e; }\n  .btn-volver:focus-visible,.btn-imprimir:focus-visible,.btn-editar:focus-visible{\n    outline:2px solid var(--azul-nav);\n    outline-offset:2px;\n  }\n  .libro-ampliado{\n    background:#fff;\n    border:1px solid var(--borde);\n    border-top:8px solid var(--celeste);\n    border-radius:14px;\n    box-shadow:0 10px 28px rgba(34,64,155,.12);\n    padding:28px 30px 32px;\n  }\n  .ampliado-titulo{\n    font-size:clamp(22px,4vw,30px);\n    line-height:1.2;\n    color:var(--azul-ifdc);\n    margin:0;\n  }\n  .libro-ampliado .ficha{ font-size:17px; gap:12px 20px; margin-top:22px; }\n  .libro-ampliado .ficha dt{ font-size:14.5px; }\n  .ficha dd{ overflow-wrap:anywhere; }\n\n  .ficha{\n    display:grid;\n    grid-template-columns:auto 1fr;\n    gap:7px 12px;\n    margin-top:14px;\n    font-size:14.5px;\n  }\n  .ficha dt{\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    color:var(--texto-suave);\n    font-size:13px;\n  }\n  .ficha dd{\n    margin:0;\n    color:var(--texto);\n  }\n\n  \/* ---------- Mensajes de estado ---------- *\/\n  .mensaje{\n    text-align:center;\n    padding:40px 20px;\n    color:var(--texto-suave);\n    font-size:15px;\n  }\n  .mensaje strong{ color:var(--texto); }\n\n  .spinner{\n    width:22px; height:22px;\n    border:3px solid var(--borde);\n    border-top-color:var(--azul-ifdc);\n    border-radius:50%;\n    margin:0 auto 12px;\n    animation:girar .8s linear infinite;\n  }\n  @keyframes girar{ to{ transform:rotate(360deg); } }\n\n  @media (prefers-reduced-motion:reduce){\n    .libro,.libro-flecha,.spinner{ transition:none; animation:none; }\n  }\n\n  \/* ---------- Modal (login \/ formulario) ---------- *\/\n  .fondo-modal{\n    position:fixed; inset:0;\n    background:rgba(23,26,45,.55);\n    display:flex; align-items:flex-start; justify-content:center;\n    padding:40px 16px;\n    overflow-y:auto;\n    z-index:50;\n  }\n  .fondo-modal[hidden]{ display:none; }\n  .caja-modal{\n    background:#fff;\n    border-radius:14px;\n    max-width:480px;\n    width:100%;\n    padding:26px 26px 24px;\n    box-shadow:0 20px 50px rgba(0,0,0,.25);\n  }\n  .caja-modal.ancho{ max-width:620px; }\n  .caja-modal h2{\n    margin:0 0 6px;\n    color:var(--azul-ifdc);\n    font-size:21px;\n  }\n  .caja-modal p.desc{ margin:0 0 18px; color:var(--texto-suave); font-size:14px; }\n  .campo{ margin-bottom:14px; }\n  .campo label{\n    display:block;\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    font-size:13.5px;\n    color:var(--texto-suave);\n    margin-bottom:5px;\n  }\n  .campo input{\n    width:100%;\n    border:1.5px solid var(--borde);\n    border-radius:8px;\n    padding:10px 12px;\n    font-size:15px;\n    font-family:Calibri,Candara,sans-serif;\n    outline:none;\n  }\n  .campo input:focus{ border-color:var(--azul-nav); }\n  .rejilla-form{\n    display:grid;\n    grid-template-columns:1fr 1fr;\n    gap:0 14px;\n  }\n  .rejilla-form .campo.full{ grid-column:1 \/ -1; }\n  .acciones-modal{\n    display:flex;\n    justify-content:flex-end;\n    gap:10px;\n    margin-top:6px;\n  }\n  .btn-cancelar{\n    background:#fff;\n    color:var(--texto-suave);\n    border:1.5px solid var(--borde);\n    border-radius:9px;\n    padding:10px 18px;\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    cursor:pointer;\n  }\n  .btn-cancelar:hover{ background:#f2f1ec; }\n  .btn-guardar{\n    background:var(--azul-ifdc);\n    color:#fff;\n    border:none;\n    border-radius:9px;\n    padding:10px 20px;\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    cursor:pointer;\n  }\n  .btn-guardar:hover{ background:var(--azul-ifdc-oscuro); }\n  .btn-guardar:disabled{ opacity:.6; cursor:default; }\n  .btn-eliminar-confirmar{\n    background:var(--rojo);\n    color:#fff;\n    border:none;\n    border-radius:9px;\n    padding:10px 20px;\n    font-family:\"Barlow\",sans-serif;\n    font-weight:600;\n    cursor:pointer;\n  }\n  .btn-eliminar-confirmar:hover{ background:var(--rojo-oscuro); }\n  .btn-eliminar-confirmar:disabled{ opacity:.6; cursor:default; }\n  .aviso-form{\n    font-size:13.5px;\n    margin-top:10px;\n    min-height:16px;\n  }\n  .aviso-form.error{ color:#b3261e; }\n  .aviso-form.ok{ color:#1c7a3d; }\n\n  @media (max-width:760px){\n    .btn-bibliotecarios{ position:static; display:inline-block; margin:0 0 14px; }\n  }\n  @media (max-width:640px){\n    .encabezado{ padding:24px 16px 50px; }\n    .btn-bibliotecarios{ position:static; display:inline-block; margin-top:14px; }\n    .caja-busqueda{ flex-direction:column; }\n    .caja-busqueda button{ padding:11px; }\n    .libro-ampliado{ padding:20px 18px 24px; }\n    .barra-acciones button{ flex:1; }\n    .rejilla-form{ grid-template-columns:1fr; }\n  }\n\n  \/* ---------- Pesta\u00f1as del modo bibliotecario ---------- *\/\n  .pestanas{ display:flex; gap:8px; justify-content:center; flex-wrap:wrap; margin-top:18px; }\n  .pestanas[hidden]{ display:none; }\n  .pestanas button{\n    background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.55); color:#fff;\n    border-radius:999px; padding:9px 24px; font-family:\"Barlow\",sans-serif; font-weight:600;\n    font-size:15px; cursor:pointer;\n  }\n  .pestanas button:hover{ background:rgba(255,255,255,.28); }\n  .pestanas button.activa{ background:#fff; color:var(--azul-ifdc); border-color:#fff; }\n  #vistaCatalogo[hidden], #vistaPrestamos[hidden], #vistaInforme[hidden]{ display:none; }\n\n  \/* ---------- Pr\u00e9stamos \/ socios ---------- *\/\n  .barra-socios{\n    max-width:720px; margin:16px auto 0; padding:0 20px;\n    display:flex; justify-content:space-between; align-items:center; gap:10px;\n  }\n  .barra-socios .etiqueta{ font-family:\"Barlow\",sans-serif; font-weight:600; font-size:14px; color:var(--texto-suave); }\n  .socio-ficha{\n    background:#fff; border:1px solid var(--borde); border-left:6px solid var(--azul-ifdc);\n    border-radius:12px; padding:22px 24px 24px;\n  }\n  .socio-ficha h2{ margin:0 0 14px; color:var(--azul-ifdc); font-size:22px; }\n  .socio-ficha h3{ margin:22px 0 4px; color:var(--azul-ifdc); font-size:17px; }\n  .socio-ficha dl{ display:grid; grid-template-columns:150px 1fr; gap:6px 14px; margin:0; }\n  .socio-ficha dt{ font-weight:700; color:var(--texto-suave); font-size:14px; }\n  .socio-ficha dd{ margin:0; word-break:break-word; }\n  .prest-fila{\n    display:grid; grid-template-columns:90px 1fr 140px auto; gap:8px; align-items:center;\n    padding:8px 0; border-bottom:1px dashed var(--borde);\n  }\n  .prest-fila .prest-n{ font-family:\"Barlow\",sans-serif; font-weight:600; color:var(--texto-suave); }\n  .prest-fila input{\n    border:1px solid var(--borde); border-radius:8px; padding:9px 10px; font-size:15px;\n    font-family:inherit; color:var(--texto); min-width:0; width:100%;\n  }\n  .prest-fila input:focus{ outline:none; border-color:var(--azul-nav); }\n  .prest-fila .btn-quitar{\n    background:#fff; border:1px solid var(--borde); border-radius:8px; padding:8px 12px;\n    cursor:pointer; color:var(--texto-suave); font-family:inherit;\n  }\n  .prest-fila .prest-libro{ grid-column:2 \/ -1; margin:-2px 0 0; font-size:13px; color:var(--texto-suave); min-height:0; }\n  .prest-fila .prest-libro:empty{ display:none; }\n  .prest-fila .prest-libro.aviso{ color:#b3261e; }\n  .prest-acciones{ display:flex; align-items:center; gap:14px; margin-top:16px; flex-wrap:wrap; }\n\n  \/* ---------- Informe ---------- *\/\n  .informe-botones{ max-width:720px; margin:22px auto 0; padding:0 20px; display:flex; gap:10px; flex-wrap:wrap; justify-content:center; }\n  .informe-botones button{\n    background:#fff; color:var(--azul-ifdc); border:2px solid var(--azul-ifdc); border-radius:999px;\n    padding:10px 22px; font-family:\"Barlow\",sans-serif; font-weight:700; font-size:15px; cursor:pointer;\n  }\n  .informe-botones button.activo{ background:var(--azul-ifdc); color:#fff; }\n  .inf-resumen{ font-family:\"Barlow\",sans-serif; font-weight:600; color:var(--texto-suave); margin:0 0 12px; font-size:14px; }\n  .inf-socio{\n    background:#fff; border:1px solid var(--borde); border-left:6px solid var(--azul-ifdc);\n    border-radius:12px; padding:14px 18px; margin-bottom:12px;\n  }\n  .inf-socio h3{ margin:0 0 6px; color:var(--azul-ifdc); font-size:18px; }\n  .inf-socio h3 small{ font-weight:400; color:var(--texto-suave); font-size:13px; }\n  .inf-lista{ list-style:none; margin:0; padding:0; }\n  .inf-lista li{ padding:6px 0; border-top:1px dashed var(--borde); font-size:15px; }\n  .inf-lista li:first-child{ border-top:none; }\n  .inf-inv{ font-family:\"Barlow\",sans-serif; font-weight:700; color:var(--azul-nav); }\n  .inf-item{\n    background:#fff; border:1px solid var(--borde); border-left:6px solid var(--azul-ifdc);\n    border-radius:12px; padding:12px 16px; margin-bottom:10px;\n  }\n  .inf-item p{ margin:0; }\n  .inf-item .inf-sub{ font-size:14px; color:var(--texto-suave); margin-top:2px; }\n  .inf-item.vencido{ background:#fdecea; border-color:#e8b4af; border-left-color:#b3261e; }\n  .inf-item.vencido .inf-titulo, .inf-item.vencido .inf-sub, .inf-item.vencido .inf-inv{ color:#b3261e; }\n  .inf-item .inf-titulo{ font-weight:600; }\n\n  \/* ---------- Etiquetas \"Prestado N\u00b0 ...\" ---------- *\/\n  .prestado-fila{ display:block; margin:0 0 5px; }\n  .ampliado-titulo + .prestado-fila{ margin:-4px 0 14px; }\n  .etiqueta-prestado{\n    display:inline-block; background:#c62828; color:#fff; font-family:\"Barlow\",sans-serif;\n    font-weight:600; font-size:12.5px; line-height:1.3; padding:3px 11px; border-radius:999px;\n    margin:0 6px 4px 0; white-space:nowrap;\n  }\n\n  \/* ---------- Devolver \/ Renovar \/ Historial ---------- *\/\n  .prest-botones{ display:flex; gap:6px; }\n  .prest-botones button{\n    border-radius:8px; padding:8px 12px; font-family:inherit; font-weight:600; font-size:14px;\n    cursor:pointer; border:1px solid var(--borde); background:#fff; color:var(--texto-suave);\n  }\n  .prest-botones .btn-renovar{ color:#9a5b00; border-color:#e6b566; background:#fff7e6; }\n  .prest-botones .btn-devolver{ color:#fff; background:#2e7d32; border-color:#2e7d32; }\n  .prest-botones button:disabled{ opacity:.4; cursor:not-allowed; }\n  .prest-fila.renovada{ background:#fff4d6; border-left:5px solid #f0a020; padding-left:10px; border-radius:6px; }\n  .prest-estado{ grid-column:2 \/ -1; margin:-2px 0 0; font-size:13px; color:var(--texto-suave); }\n  .prest-estado:empty{ display:none; }\n  .prest-estado.renov{ color:#9a5b00; font-weight:600; }\n  .prest-estado.venc{ color:#b3261e; font-weight:600; }\n  .inf-item.renovado{ background:#fff4d6; border-color:#f0c878; border-left-color:#f0a020; }\n  .inf-item.renovado .inf-sub{ color:#9a5b00; }\n  .inf-item.devuelto{ border-left-color:#7a8aa0; }\n  .inf-item .inf-en-curso{ font-weight:700; color:#c62828; }\n  #vistaHistorial[hidden]{ display:none; }\n\n  .campo select{\n    width:100%; padding:11px 12px; border:1px solid var(--borde); border-radius:10px;\n    font-family:inherit; font-size:15px; color:var(--texto); background:#fff;\n  }\n  .campo select:focus{ outline:none; border-color:var(--azul-nav); }\n\n  .pestanas .btn-refrescar{ padding:9px 13px; display:inline-flex; align-items:center; justify-content:center; }\n  .pestanas .btn-refrescar svg{ display:block; }\n  .pestanas .btn-refrescar.girando svg{ animation:girar .8s linear infinite; }\n  .pestanas .btn-refrescar:disabled{ cursor:default; }\n  .toast{\n    position:fixed; left:50%; bottom:22px; transform:translateX(-50%); z-index:60;\n    background:#2f3340; color:#fff; font-family:\"Barlow\",sans-serif; font-size:14px; font-weight:600;\n    padding:10px 18px; border-radius:999px; box-shadow:0 6px 20px rgba(0,0,0,.25); max-width:90%; text-align:center;\n  }\n  .toast[hidden]{ display:none; }\n  .toast.error{ background:#b3261e; }\n\n  \/* ---------- Botones ocultos seg\u00fan el perfil ---------- *\/\n  body.sin-catalogo #btnAgregarLibro{ display:none !important; }\n  body.sin-socios #btnNuevoSocio,\n  body.sin-socios #btnEditarSocio{ display:none !important; }\n  body.sin-eliminar-socios #btnEliminarSocio{ display:none !important; }\n  body.sin-prestamos #btnGuardarPrestamos,\n  body.sin-prestamos .prest-botones{ display:none !important; }\n  body.sin-mensajes .com-nuevo{ display:none !important; }\n\n  \/* ---------- Bot\u00f3n Guardar junto a los dem\u00e1s botones del socio ---------- *\/\n  .barra-acciones .btn-guardar{\n    font-size:15px; border:2px solid var(--azul-ifdc); border-radius:9px; padding:10px 18px;\n  }\n\n  \/* ---------- Comunicaci\u00f3n ---------- *\/\n  #vistaComunicacion[hidden]{ display:none; }\n  .com-nuevo{ max-width:720px; margin:22px auto 0; padding:0 20px; }\n  .com-nuevo label{ display:block; margin-bottom:6px; font-family:\"Barlow\",sans-serif; font-weight:600; color:var(--texto-suave); }\n  .com-nuevo textarea{\n    width:100%; box-sizing:border-box; min-height:90px; resize:vertical; background:#fff;\n    border:1px solid var(--borde); border-radius:12px; padding:12px; font:inherit; font-size:15px; color:var(--texto);\n  }\n  .com-nuevo textarea:focus{ outline:none; border-color:var(--azul-nav); }\n  .com-acciones{ display:flex; justify-content:space-between; align-items:center; gap:10px; margin-top:8px; flex-wrap:wrap; }\n  .com-contador, .com-nota{ font-size:13px; color:var(--texto-suave); }\n  .com-nota{ margin:8px 0 0; }\n  .com-msg{\n    background:#fff; border:1px solid var(--borde); border-left:6px solid var(--azul-nav);\n    border-radius:12px; padding:12px 16px; margin-bottom:10px;\n  }\n  \/* un color tenue distinto por usuario *\/\n  .com-c0{ background:#eef1fb; border-color:#d3dbf5; border-left-color:#6d7fd6; }\n  .com-c1{ background:#fdf1e3; border-color:#f3dcbd; border-left-color:#e8a85c; }\n  .com-c2{ background:#e9f5ec; border-color:#c9e5d0; border-left-color:#6fb482; }\n  .com-c3{ background:#f3ecfa; border-color:#e0d1f0; border-left-color:#a67fd0; }\n  .com-c4{ background:#fff8dc; border-color:#f1e5a8; border-left-color:#d9b93c; }\n  .com-c5{ background:#fdeaee; border-color:#f4c9d3; border-left-color:#e07a93; }\n  .com-c6{ background:#e6f4f6; border-color:#c3e3e8; border-left-color:#5aaab8; }\n  .com-c7{ background:#f1efe8; border-color:#dcd8c8; border-left-color:#a39a82; }\n  .com-c8{ background:#eaf0e0; border-color:#d3e0bd; border-left-color:#93b05a; }\n  .com-c9{ background:#fbe9e1; border-color:#f1cdbd; border-left-color:#dd8a6a; }\n  .com-meta{ margin:0 0 4px; font-size:13px; color:var(--texto-suave); }\n  .com-texto{ margin:0; white-space:pre-wrap; word-break:break-word; }\n  .com-mas{ max-width:720px; margin:6px auto 30px; padding:0 20px; text-align:center; }\n  .com-mas button{\n    background:#fff; color:var(--azul-ifdc); border:2px solid var(--azul-ifdc); border-radius:999px;\n    padding:10px 24px; font-family:\"Barlow\",sans-serif; font-weight:700; font-size:15px; cursor:pointer;\n  }\n  .com-mas button:disabled{ opacity:.5; cursor:default; }\n\n  \/* ---------- Pr\u00e9stamos y fichas en celulares (dise\u00f1o m\u00f3vil por defecto) ---------- *\/\n  .socio-ficha{ min-width:0; padding:18px 14px 20px; }\n  .socio-ficha dl{ grid-template-columns:1fr; gap:0; }\n  .socio-ficha dd{ margin-bottom:8px; }\n  .barra-acciones{ flex-wrap:wrap; gap:10px; }\n  .barra-acciones .grupo{ width:100%; }\n  .barra-acciones .grupo button{ flex:1 1 auto; }\n  .prest-fila{ grid-template-columns:minmax(0,1fr) minmax(0,1fr); padding:12px 0; }\n  .prest-fila .prest-n{ grid-column:1 \/ -1; }\n  .prest-fila .prest-botones{ grid-column:1 \/ -1; }\n  .prest-fila .prest-libro,\n  .prest-fila .prest-estado{ grid-column:1 \/ -1; margin:0; overflow-wrap:anywhere; }\n  .prest-fila input{ padding:11px 10px; }\n  .prest-botones button{ flex:1 1 0; min-height:42px; }\n  @media (min-width:641px){\n    .socio-ficha{ padding:22px 24px 24px; }\n    .socio-ficha dl{ grid-template-columns:150px 1fr; gap:6px 14px; }\n    .socio-ficha dd{ margin-bottom:0; }\n    .barra-acciones .grupo{ width:auto; }\n    .prest-fila{ grid-template-columns:90px minmax(0,1fr) 140px auto; }\n    .prest-fila .prest-n{ grid-column:auto; }\n    .prest-fila .prest-botones{ grid-column:auto; }\n    .prest-fila .prest-libro,\n    .prest-fila .prest-estado{ grid-column:2 \/ -1; }\n    .prest-botones button{ flex:0 0 auto; min-height:0; }\n  }\n<\/style>\n<\/head>\n<body>\n\n<header class=\"encabezado\">\n  <button type=\"button\" class=\"btn-bibliotecarios\" id=\"btnBibliotecarios\">Acceso bibliotecarios<\/button>\n  <p class=\"rotulo\">Instituto de Formaci\u00f3n Docente Continua \u00b7 Villa Regina<\/p>\n  <h1>BILIB &#8211; Cat\u00e1logo en L\u00ednea<\/h1>\n  <p>Escrib\u00ed un t\u00edtulo, un autor, un tema o un n\u00famero de inventario y te mostramos los libros del cat\u00e1logo que coinciden.<\/p>\n  <div class=\"pestanas\" id=\"pestanasBib\" hidden>\n    <button type=\"button\" class=\"btn-refrescar\" id=\"btnRefrescar\" title=\"Actualizar datos\" aria-label=\"Actualizar datos\">\n      <svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"23 4 23 10 17 10\"><\/polyline><polyline points=\"1 20 1 14 7 14\"><\/polyline><path d=\"M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15\"><\/path><\/svg>\n    <\/button>\n    <button type=\"button\" class=\"activa\" id=\"tabCatalogo\">Cat\u00e1logo<\/button>\n    <button type=\"button\" id=\"tabPrestamos\">Pr\u00e9stamos<\/button>\n    <button type=\"button\" id=\"tabInforme\">Informe<\/button>\n    <button type=\"button\" id=\"tabHistorial\">Historial<\/button>\n    <button type=\"button\" id=\"tabComunicacion\">Comunicaci\u00f3n<\/button>\n  <\/div>\n<\/header>\n\n<div id=\"vistaCatalogo\">\n<div class=\"panel-busqueda\">\n  <form class=\"caja-busqueda\" id=\"formBusqueda\" role=\"search\">\n    <label for=\"campoBusqueda\" class=\"visually-hidden\" style=\"position:absolute;left:-9999px;\">Buscar en el cat\u00e1logo<\/label>\n    <input type=\"text\" id=\"campoBusqueda\" placeholder=\"Ej: Caperucita, Ortiz, N\u00b0 de inventario...\" autocomplete=\"off\">\n    <button type=\"submit\">Buscar<\/button>\n  <\/form>\n<\/div>\n\n<p class=\"estado-busqueda\" id=\"estadoBusqueda\"><\/p>\n\n<div class=\"barra-bibliotecario\" id=\"barraBibliotecario\" hidden>\n  <span class=\"etiqueta\">Modo bibliotecario activo<\/span>\n  <button type=\"button\" class=\"btn-agregar\" id=\"btnAgregarLibro\">+ Cargar libro nuevo<\/button>\n<\/div>\n\n<main class=\"resultados\" id=\"resultados\">\n  <div class=\"mensaje\" id=\"mensajeInicial\">\n    <div class=\"spinner\"><\/div>\n    Cargando el cat\u00e1logo\u2026\n  <\/div>\n<\/main>\n<\/div><!-- \/vistaCatalogo -->\n\n<!-- ---------- Vista Pr\u00e9stamos (solo bibliotecarios) ---------- -->\n<div id=\"vistaPrestamos\" hidden>\n  <div class=\"panel-busqueda\">\n    <form class=\"caja-busqueda\" id=\"formSocios\" role=\"search\">\n      <label for=\"campoSocios\" style=\"position:absolute;left:-9999px;\">Buscar socios<\/label>\n      <input type=\"text\" id=\"campoSocios\" placeholder=\"Buscar socio por DNI, apellido, nombre o N\u00b0 de inventario...\" autocomplete=\"off\">\n      <button type=\"submit\">Buscar<\/button>\n    <\/form>\n  <\/div>\n  <p class=\"estado-busqueda\" id=\"estadoSocios\"><\/p>\n  <div class=\"barra-socios\">\n    <span class=\"etiqueta\">Socios y pr\u00e9stamos<\/span>\n    <button type=\"button\" class=\"btn-agregar\" id=\"btnNuevoSocio\">+ Nuevo socio<\/button>\n  <\/div>\n  <main class=\"resultados\" id=\"listaSocios\"><\/main>\n<\/div>\n\n<!-- ---------- Vista Informe (solo bibliotecarios) ---------- -->\n<div id=\"vistaInforme\" hidden>\n  <div class=\"informe-botones\">\n    <button type=\"button\" id=\"btnInfSocios\">Socios con Pr\u00e9stamos<\/button>\n    <button type=\"button\" id=\"btnInfTodos\">Todos los Pr\u00e9stamos<\/button>\n  <\/div>\n  <p class=\"estado-busqueda\" id=\"estadoInforme\"><\/p>\n  <main class=\"resultados\" id=\"resultadoInforme\">\n    <div class=\"mensaje\">Eleg\u00ed un informe con los botones de arriba.<\/div>\n  <\/main>\n<\/div>\n\n<!-- ---------- Vista Historial (solo bibliotecarios) ---------- -->\n<div id=\"vistaHistorial\" hidden>\n  <div class=\"panel-busqueda\">\n    <form class=\"caja-busqueda\" id=\"formHistorial\" role=\"search\">\n      <label for=\"campoHistorial\" style=\"position:absolute;left:-9999px;\">Buscar en el historial<\/label>\n      <input type=\"text\" id=\"campoHistorial\" placeholder=\"Historial de un libro (t\u00edtulo o N\u00b0 de inventario) o de un socio...\" autocomplete=\"off\">\n      <button type=\"submit\">Buscar<\/button>\n    <\/form>\n  <\/div>\n  <p class=\"estado-busqueda\" id=\"estadoHistorial\"><\/p>\n  <main class=\"resultados\" id=\"resultadoHistorial\">\n    <div class=\"mensaje\">Escrib\u00ed un libro o un socio para ver su historial de pr\u00e9stamos.<\/div>\n  <\/main>\n<\/div>\n\n<!-- ---------- Modal de login ---------- -->\n<div class=\"fondo-modal\" id=\"modalLogin\" hidden>\n  <div class=\"caja-modal\">\n    <h2>Acceso bibliotecarios<\/h2>\n    <p class=\"desc\">Ingres\u00e1 tu usuario y tu contrase\u00f1a.<\/p>\n    <form id=\"formLogin\">\n      <div class=\"campo\">\n        <label for=\"usuarioLogin\">Usuario<\/label>\n        <input type=\"text\" id=\"usuarioLogin\" autocomplete=\"username\" autocapitalize=\"none\" required>\n      <\/div>\n      <div class=\"campo\">\n        <label for=\"claveLogin\">Contrase\u00f1a<\/label>\n        <input type=\"password\" id=\"claveLogin\" autocomplete=\"current-password\" required>\n      <\/div>\n      <p class=\"aviso-form error\" id=\"avisoLogin\"><\/p>\n      <div class=\"acciones-modal\">\n        <button type=\"button\" class=\"btn-cancelar\" id=\"btnCancelarLogin\">Cancelar<\/button>\n        <button type=\"submit\" class=\"btn-guardar\" id=\"btnIngresarLogin\">Ingresar<\/button>\n      <\/div>\n    <\/form>\n  <\/div>\n<\/div>\n\n<!-- ---------- Modal de formulario de libro (alta \/ edici\u00f3n) ---------- -->\n<div class=\"fondo-modal\" id=\"modalLibro\" hidden>\n  <div class=\"caja-modal ancho\">\n    <h2 id=\"tituloModalLibro\">Cargar libro nuevo<\/h2>\n    <p class=\"desc\" id=\"descModalLibro\">Se agrega debajo de la \u00faltima fila con datos de la planilla.<\/p>\n    <form id=\"formLibro\">\n      <div class=\"rejilla-form\">\n        <div class=\"campo full\">\n          <label for=\"fTitulo\">T\u00edtulo *<\/label>\n          <input type=\"text\" id=\"fTitulo\" required>\n        <\/div>\n        <div class=\"campo\"><label for=\"fAutor\">Autor principal *<\/label><input type=\"text\" id=\"fAutor\" required><\/div>\n        <div class=\"campo\"><label for=\"fAutoresSec\">Autores secundarios<\/label><input type=\"text\" id=\"fAutoresSec\"><\/div>\n        <div class=\"campo\"><label for=\"fEditorial\">Editorial<\/label><input type=\"text\" id=\"fEditorial\"><\/div>\n        <div class=\"campo\"><label for=\"fAnio\">A\u00f1o<\/label><input type=\"text\" id=\"fAnio\"><\/div>\n        <div class=\"campo\"><label for=\"fIsbn\">ISBN<\/label><input type=\"text\" id=\"fIsbn\"><\/div>\n        <div class=\"campo\"><label for=\"fSeccion\">Secci\u00f3n<\/label><input type=\"text\" id=\"fSeccion\"><\/div>\n        <div class=\"campo\"><label for=\"fSignatura\">Signatura(s) *<\/label><input type=\"text\" id=\"fSignatura\" required><\/div>\n        <div class=\"campo\"><label for=\"fInventario\">N\u00b0 de inventario *<\/label><input type=\"text\" id=\"fInventario\" required><\/div>\n        <div class=\"campo\"><label for=\"fCantidad\">Cantidad de ejemplares *<\/label><input type=\"text\" id=\"fCantidad\" required><\/div>\n        <div class=\"campo\"><label for=\"fEstado\">Estado \/ Condici\u00f3n<\/label><input type=\"text\" id=\"fEstado\"><\/div>\n        <div class=\"campo full\"><label for=\"fDescriptores\">Descriptores<\/label><input type=\"text\" id=\"fDescriptores\"><\/div>\n        <div class=\"campo full\"><label for=\"fObserv\">Observaciones<\/label><input type=\"text\" id=\"fObserv\"><\/div>\n      <\/div>\n      <p class=\"aviso-form\" id=\"avisoLibro\"><\/p>\n      <div class=\"acciones-modal\">\n        <button type=\"button\" class=\"btn-cancelar\" id=\"btnCancelarLibro\">Cancelar<\/button>\n        <button type=\"submit\" class=\"btn-guardar\" id=\"btnGuardarLibro\">Guardar<\/button>\n      <\/div>\n    <\/form>\n  <\/div>\n<\/div>\n\n<!-- ---------- Modal de confirmaci\u00f3n de borrado ---------- -->\n<div class=\"fondo-modal\" id=\"modalConfirmar\" hidden>\n  <div class=\"caja-modal\">\n    <h2>Eliminar libro<\/h2>\n    <p class=\"desc\" id=\"descConfirmar\">\u00bfSeguro que quer\u00e9s eliminar este libro del cat\u00e1logo? Esta acci\u00f3n no se puede deshacer.<\/p>\n    <p class=\"aviso-form\" id=\"avisoConfirmar\"><\/p>\n    <div class=\"acciones-modal\">\n      <button type=\"button\" class=\"btn-cancelar\" id=\"btnCancelarEliminar\">Cancelar<\/button>\n      <button type=\"button\" class=\"btn-eliminar-confirmar\" id=\"btnConfirmarEliminar\">Eliminar<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- ---------- Modal de socio (alta \/ edici\u00f3n) ---------- -->\n<div class=\"fondo-modal\" id=\"modalSocio\" hidden>\n  <div class=\"caja-modal ancho\">\n    <h2 id=\"tituloModalSocio\">Nuevo socio<\/h2>\n    <p class=\"desc\" id=\"descModalSocio\"><\/p>\n    <form id=\"formSocio\">\n      <div class=\"rejilla-form\">\n        <div class=\"campo\"><label for=\"sDni\">DNI *<\/label><input type=\"text\" id=\"sDni\" required><\/div>\n        <div class=\"campo\"><label for=\"sApellido\">Apellido *<\/label><input type=\"text\" id=\"sApellido\" required><\/div>\n        <div class=\"campo\"><label for=\"sNombre\">Nombre *<\/label><input type=\"text\" id=\"sNombre\" required><\/div>\n        <div class=\"campo\"><label for=\"sTelefono\">Tel\u00e9fono<\/label><input type=\"text\" id=\"sTelefono\"><\/div>\n        <div class=\"campo full\"><label for=\"sMail\">Mail *<\/label><input type=\"email\" id=\"sMail\" required><\/div>\n        <div class=\"campo\"><label for=\"sCarrera\">Carrera<\/label>\n          <select id=\"sCarrera\"><option value=\"\">Seleccionar&#8230;<\/option><option value=\"Primaria\">Primaria<\/option><option value=\"Especial\">Especial<\/option><option value=\"Lengua y Literatura\">Lengua y Literatura<\/option><\/select><\/div>\n        <div class=\"campo\"><label for=\"sUsuario\">Usuario<\/label>\n          <select id=\"sUsuario\"><option value=\"\">Seleccionar&#8230;<\/option><option value=\"Estudiante\">Estudiante<\/option><option value=\"Docente\">Docente<\/option><option value=\"No Docente\">No Docente<\/option><\/select><\/div>\n      <\/div>\n      <p class=\"aviso-form\" id=\"avisoSocio\"><\/p>\n      <div class=\"acciones-modal\">\n        <button type=\"button\" class=\"btn-cancelar\" id=\"btnCancelarSocio\">Cancelar<\/button>\n        <button type=\"submit\" class=\"btn-guardar\" id=\"btnGuardarSocio\">Guardar<\/button>\n      <\/div>\n    <\/form>\n  <\/div>\n<\/div>\n\n<!-- ---------- Confirmar eliminar socio ---------- -->\n<div class=\"fondo-modal\" id=\"modalConfirmarSocio\" hidden>\n  <div class=\"caja-modal\">\n    <h2>Eliminar socio<\/h2>\n    <p class=\"desc\" id=\"descConfirmarSocio\"><\/p>\n    <p class=\"aviso-form\" id=\"avisoConfirmarSocio\"><\/p>\n    <div class=\"acciones-modal\">\n      <button type=\"button\" class=\"btn-cancelar\" id=\"btnCancelarEliminarSocio\">Cancelar<\/button>\n      <button type=\"button\" class=\"btn-eliminar-confirmar\" id=\"btnConfirmarEliminarSocio\">Eliminar<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- ---------- Vista Comunicaci\u00f3n (solo bibliotecarios) ---------- -->\n<div id=\"vistaComunicacion\" hidden>\n  <div class=\"com-nuevo\">\n    <label for=\"textoMensaje\">Nuevo mensaje<\/label>\n    <textarea id=\"textoMensaje\" maxlength=\"1000\" rows=\"3\" placeholder=\"Escrib\u00ed un mensaje para el equipo de la biblioteca...\"><\/textarea>\n    <div class=\"com-acciones\">\n      <span class=\"com-contador\" id=\"contadorMensaje\">0 \/ 1000<\/span>\n      <button type=\"button\" class=\"btn-guardar\" id=\"btnEnviarMensaje\">Enviar<\/button>\n    <\/div>\n    <p class=\"aviso-form\" id=\"avisoMensaje\"><\/p>\n    <p class=\"com-nota\">Los mensajes quedan con fecha y hora, y no se pueden editar ni borrar una vez enviados.<\/p>\n  <\/div>\n  <p class=\"estado-busqueda\">\u00daltimos mensajes<\/p>\n  <div class=\"resultados\" id=\"listaMensajes\"><\/div>\n  <div class=\"com-mas\"><button type=\"button\" id=\"btnMasMensajes\" hidden>+ Mensajes<\/button><\/div>\n<\/div>\n\n<div class=\"toast\" id=\"toast\" role=\"status\" hidden><\/div>\n\n<script>\n\/\/ Diagn\u00f3stico: si algo falla al iniciar, lo muestra en pantalla en lugar de quedar \"cargando\".\n(function(){\n  var AMP = String.fromCharCode(38), LT = String.fromCharCode(60);\n  function esc(t){ return String(t).split(AMP).join(AMP + \"amp;\").split(LT).join(AMP + \"lt;\"); }\n  function aviso(txt){\n    var el = document.getElementById(\"resultados\");\n    if (el ? el.querySelector(\".spinner\") : null){\n      el.innerHTML = '<div class=\"mensaje\"><strong>No se pudo iniciar el buscador.<\/strong><br>' +\n        'Detalle t\u00e9cnico: ' + esc(txt) + '<\/div>';\n    }\n  }\n  window.addEventListener(\"error\", function(e){\n    var base = (e.message || \"error\") + (e.lineno ? \" (l\u00ednea \" + e.lineno + \", columna \" + (e.colno || \"?\") + \")\" : \"\");\n    aviso(base);\n    \/\/ Busca en el c\u00f3digo fuente de la p\u00e1gina el trozo exacto que da error.\n    if (e.lineno ? window.fetch : false){\n      fetch(location.href, { cache: \"no-store\" })\n        .then(function(r){ return r.text(); })\n        .then(function(t){\n          var linea = t.split(\/\\r\\n|\\r|\\n\/)[e.lineno - 1] || \"\";\n          var c = e.colno || 1;\n          var trozo = linea.substring(Math.max(0, c - 80), c + 80);\n          var el = document.getElementById(\"resultados\");\n          if (el ? !el.querySelector(\"[data-fuente]\") : false){\n            el.insertAdjacentHTML(\"beforeend\",\n              '<div class=\"mensaje\" data-fuente=\"1\" style=\"font-size:12px;text-align:left;word-break:break-all;\">' +\n              'Trozo con error: <code>' + esc(trozo) + '<\/code><\/div>');\n          }\n        })\n        .catch(function(){});\n    }\n  });\n  window.addEventListener(\"unhandledrejection\", function(e){\n    aviso((e.reason ? e.reason.message : \"\") || e.reason);\n  });\n  setTimeout(function(){\n    if (!window.__bibIniciado) aviso(\"el script principal no se ejecut\u00f3 (puede estar bloqueado o cortado por WordPress o un plugin de cach\u00e9\/minificaci\u00f3n).\");\n  }, 4000);\n})();\n<\/script>\n<script>\n(function(){\n  window.__bibIniciado = true;\n\n  \/\/ Mensaje de un error (sin usar operadores que WordPress pueda alterar).\n  function msgErr(err, porDefecto){\n    if (err){ if (err.message) return err.message; }\n    return porDefecto;\n  }\n\n  \/\/ ID de la planilla de Google Sheets con el cat\u00e1logo de libros.\n  var ID_PLANILLA = \"1EkTPrYp-o5NhHBOCXxrWaMx9nXt1zAH8BBuSImCfErc\";\n  \/\/ Nombre de la hoja (pesta\u00f1a) donde est\u00e1n los datos.\n  var NOMBRE_HOJA = \"Cat\u00e1logo\";\n  \/\/ URL del Apps Script publicado (backend de carga\/edici\u00f3n).\n  var URL_APPS_SCRIPT = \"https:\/\/script.google.com\/macros\/s\/AKfycbyFk4aRj2U3AZ-sahK-np-EO6fvSgnDPBwM_4UTSsWlitVRQu5Houd1oVmnSL3KU0v7\/exec\";\n\n  var elResultados = document.getElementById(\"resultados\");\n  var elEstado = document.getElementById(\"estadoBusqueda\");\n  var elCampo = document.getElementById(\"campoBusqueda\");\n  var elForm = document.getElementById(\"formBusqueda\");\n  var elBtnBibliotecarios = document.getElementById(\"btnBibliotecarios\");\n  var elBarraBibliotecario = document.getElementById(\"barraBibliotecario\");\n  var elBtnAgregarLibro = document.getElementById(\"btnAgregarLibro\");\n\n  var elModalLogin = document.getElementById(\"modalLogin\");\n  var elFormLogin = document.getElementById(\"formLogin\");\n  var elClaveLogin = document.getElementById(\"claveLogin\");\n  var elUsuarioLogin = document.getElementById(\"usuarioLogin\");\n  var elAvisoLogin = document.getElementById(\"avisoLogin\");\n  var elBtnCancelarLogin = document.getElementById(\"btnCancelarLogin\");\n\n  var elModalLibro = document.getElementById(\"modalLibro\");\n  var elFormLibro = document.getElementById(\"formLibro\");\n  var elTituloModalLibro = document.getElementById(\"tituloModalLibro\");\n  var elDescModalLibro = document.getElementById(\"descModalLibro\");\n  var elAvisoLibro = document.getElementById(\"avisoLibro\");\n  var elBtnCancelarLibro = document.getElementById(\"btnCancelarLibro\");\n  var elBtnGuardarLibro = document.getElementById(\"btnGuardarLibro\");\n\n  var elModalConfirmar = document.getElementById(\"modalConfirmar\");\n  var elDescConfirmar = document.getElementById(\"descConfirmar\");\n  var elAvisoConfirmar = document.getElementById(\"avisoConfirmar\");\n  var elBtnCancelarEliminar = document.getElementById(\"btnCancelarEliminar\");\n  var elBtnConfirmarEliminar = document.getElementById(\"btnConfirmarEliminar\");\n\n  var libros = [];\n  var huboError = false;\n  var listaActual = [];\n  var ultimaConsulta = \"\";\n  \/\/ true: cualquier visitante ve las etiquetas \"Prestado N\u00b0 ...\" (solo se publican los n\u00fameros de\n  \/\/ inventario, nunca datos de socios). false: solo las ve quien entr\u00f3 al modo bibliotecario.\n  var MOSTRAR_PRESTADOS_AL_PUBLICO = true;\n  var prestadosPorInv = {};\n  var scrollLista = 0;\n\n  var tokenSesion = null; \/\/ credencial temporal de la sesi\u00f3n; se guarda en memoria solo mientras dura la p\u00e1gina\n  var usuarioActual = \"\";\n  var perfilActual = \"\";\n  var permisosActuales = { catalogo: false, socios: false, eliminarSocios: false, prestamos: false, mensajes: false };\n  var filaEnEdicion = null;      \/\/ n\u00famero de fila real en la hoja, cuando se edita un libro existente\n  var libroAEliminar = null;     \/\/ libro pendiente de confirmar borrado\n\n  function normalizar(texto){\n    return (texto || \"\")\n      .toString()\n      .toLowerCase()\n      .normalize(\"NFD\").replace(\/[\\u0300-\\u036f]\/g, \"\"); \/\/ saca acentos\n  }\n\n  function textoCelda(celda){\n    if (!celda) return \"\";\n    return celda.v === null || celda.v === undefined ? \"\" : String(celda.v).trim();\n  }\n\n  function cargarCatalogo(callback, alError){\n    var url = \"https:\/\/docs.google.com\/spreadsheets\/d\/\" + ID_PLANILLA +\n              \"\/gviz\/tq?tqx=out:json\" + String.fromCharCode(38) + \"sheet=\" + encodeURIComponent(NOMBRE_HOJA);\n\n    fetch(url)\n      .then(function(resp){\n        if (!resp.ok) throw new Error(\"No se pudo acceder a la planilla (\" + resp.status + \")\");\n        return resp.text();\n      })\n      .then(function(texto){\n        \/\/ La respuesta viene envuelta en google.visualization.Query.setResponse(...);\n        var inicio = texto.indexOf(\"{\");\n        var fin = texto.lastIndexOf(\"}\");\n        var json = JSON.parse(texto.substring(inicio, fin + 1));\n        var filas = json.table.rows || [];\n\n        \/\/ La API de gviz detecta la fila de encabezados y no la incluye en\n        \/\/ \"rows\", as\u00ed que la primera fila de datos es la fila 2 de la hoja.\n        libros = filas.map(function(fila, indice){\n          var c = fila.c || [];\n          return {\n            fila:        indice + 2,\n            inventario:  textoCelda(c[0]),\n            cantidad:    textoCelda(c[1]),\n            seccion:     textoCelda(c[2]),\n            signatura:   textoCelda(c[3]),\n            titulo:      textoCelda(c[4]),\n            autor:       textoCelda(c[5]),\n            autoresSec:  textoCelda(c[6]),\n            editorial:   textoCelda(c[7]),\n            anio:        textoCelda(c[8]),\n            isbn:        textoCelda(c[9]),\n            descriptores:textoCelda(c[10]),\n            estado:      textoCelda(c[11]),\n            observ:      textoCelda(c[12])\n          };\n        }).filter(function(libro){ return libro.titulo; });\n\n        huboError = false;\n        if (callback) callback();\n        else mostrarMensajeInicial();\n      })\n      .catch(function(err){\n        huboError = true;\n        ultimoError = msgErr(err, String(err));\n        mostrarError();\n        console.error(err);\n        if (alError) alError();\n      });\n  }\n\n  \/\/ Total de libros = suma de \"Cantidad de ejemplares\" de todos los registros\n  \/\/ (si un registro no tiene la cantidad cargada, cuenta como 1 ejemplar).\n  function totalEjemplares(){\n    var total = 0;\n    for (var i = 0; i !== libros.length; i++){\n      var m = String(libros[i].cantidad).match(\/\\d+\/);\n      total += m ? parseInt(m[0], 10) : 1;\n    }\n    return total;\n  }\n\n  function mostrarMensajeInicial(){\n    var n = totalEjemplares();\n    elResultados.innerHTML =\n      '<div class=\"mensaje\">Hay <strong>' + n.toLocaleString(\"es-AR\") + '<\/strong> ' +\n      (n === 1 ? 'libro disponible' : 'libros disponibles') + ' para buscar. ' +\n      'Escrib\u00ed una palabra clave arriba para empezar.<\/div>';\n  }\n\n  var ultimoError = \"\";\n  function mostrarError(){\n    elResultados.innerHTML =\n      '<div class=\"mensaje\">No pudimos cargar el cat\u00e1logo en este momento. ' +\n      'Verific\u00e1 que la planilla est\u00e9 compartida como \"Cualquier persona con el enlace puede ver\" e intent\u00e1 de nuevo m\u00e1s tarde.' +\n      (ultimoError ? '<br><small>Detalle t\u00e9cnico: ' + escapeHtml(ultimoError) + '<\/small>' : '') + '<\/div>';\n  }\n\n  function coincide(libro, palabras){\n    var campos = normalizar([\n      libro.inventario, libro.titulo, libro.autor, libro.autoresSec,\n      libro.editorial, libro.seccion, libro.signatura, libro.descriptores,\n      libro.estado, libro.observ\n    ].join(\" \"));\n    return palabras.every(function(palabra){ return campos.indexOf(palabra) !== -1; });\n  }\n\n  function buscar(consulta){\n    elResultados.classList.remove(\"modo-detalle\");\n    ultimaConsulta = consulta;\n    if (huboError){ mostrarError(); return; }\n\n    var texto = consulta.trim();\n    if (!texto){\n      mostrarMensajeInicial();\n      elEstado.textContent = \"\";\n      return;\n    }\n\n    var palabras = normalizar(texto).split(\/\\s+\/).filter(Boolean);\n    var encontrados = libros.filter(function(libro){ return coincide(libro, palabras); });\n\n    elEstado.textContent = encontrados.length\n      ? encontrados.length + (encontrados.length === 1 ? \" resultado\" : \" resultados\") + \" para \\u201c\" + texto + \"\\u201d\"\n      : \"\";\n\n    if (!encontrados.length){\n      elResultados.innerHTML =\n        '<div class=\"mensaje\">No encontramos libros que coincidan con <strong>\\u201c' + escapeHtml(texto) + '\\u201d<\/strong>. ' +\n        'Prob\u00e1 con otra palabra, o con el apellido del autor.<\/div>';\n      return;\n    }\n\n    listaActual = encontrados;\n    elResultados.innerHTML = encontrados.map(function(libro, i){ return tarjetaLibro(libro, i); }).join(\"\");\n\n    Array.prototype.forEach.call(elResultados.querySelectorAll(\".libro-cabecera\"), function(boton){\n      boton.addEventListener(\"click\", function(){\n        mostrarDetalle(parseInt(boton.getAttribute(\"data-i\"), 10));\n      });\n    });\n  }\n\n  function escapeHtml(texto){\n    var div = document.createElement(\"div\");\n    div.textContent = texto;\n    return div.innerHTML;\n  }\n\n  function camposLibro(libro){\n    return [\n      [\"Autor principal\", libro.autor],\n      [\"Autores secundarios\", libro.autoresSec],\n      [\"Editorial\", libro.editorial],\n      [\"A\u00f1o\", libro.anio],\n      [\"Secci\u00f3n\", libro.seccion],\n      [\"Signatura(s)\", libro.signatura],\n      [\"ISBN\", libro.isbn],\n      [\"N\u00b0 de inventario\", libro.inventario],\n      [\"Cantidad de ejemplares\", libro.cantidad],\n      [\"Estado \/ Condici\u00f3n\", libro.estado],\n      [\"Descriptores\", libro.descriptores],\n      [\"Observaciones\", libro.observ]\n    ].filter(function(c){ return c[1]; });\n  }\n\n  \/\/ Botones rojos \"Prestado N\u00b0 ...\" (uno por cada ejemplar de ese libro que est\u00e1 prestado).\n  function etiquetasPrestado(libro){\n    var nums = String(libro.inventario).split(\/[,;\\n]+\/);\n    var html = \"\";\n    for (var k = 0; k !== nums.length; k++){\n      var n = nums[k].trim();\n      if (n) { if (prestadosPorInv[n] === true) html += '<span class=\"etiqueta-prestado\">Prestado N\u00b0 ' + escapeHtml(n) + '<\/span>'; }\n    }\n    return html ? '<span class=\"prestado-fila\">' + html + '<\/span>' : \"\";\n  }\n\n  \/\/ Vuelve a dibujar los resultados que se est\u00e1n viendo (si hay una b\u00fasqueda y no se est\u00e1 en una ficha).\n  function refrescarEtiquetas(){\n    if (huboError) return;\n    if (libros.length === 0) return;\n    if (elResultados.classList.contains(\"modo-detalle\")) return;\n    if (ultimaConsulta.trim() === \"\") return;\n    buscar(ultimaConsulta);\n  }\n\n  \/\/ Lee del Apps Script solo los N\u00b0 de inventario prestados (sin datos de socios).\n  function cargarPrestados(){\n    if (MOSTRAR_PRESTADOS_AL_PUBLICO ? false : !tokenSesion) return;\n    llamarServicio({ accion: \"inventariosPrestados\" })\n      .then(function(res){\n        if (!res.ok) return;\n        var mapa = {};\n        res.inventarios.forEach(function(n){ var t = String(n).trim(); if (t) mapa[t] = true; });\n        prestadosPorInv = mapa;\n        refrescarEtiquetas();\n      })\n      .catch(function(){ \/* sin conexi\u00f3n con el servicio: simplemente no se muestran las etiquetas *\/ });\n  }\n\n  \/\/ Cuando el bibliotecario ya carg\u00f3 los socios, se usa esa lista (m\u00e1s actual).\n  function prestadosDesdeSocios(){\n    var mapa = {};\n    socios.forEach(function(s){\n      s.prestamos.forEach(function(p){ if (p.inv) mapa[p.inv] = true; });\n    });\n    prestadosPorInv = mapa;\n    refrescarEtiquetas();\n  }\n\n  function tarjetaLibro(libro, indice){\n    return (\n      '<article class=\"libro\">' +\n        '<button type=\"button\" class=\"libro-cabecera\" data-i=\"' + indice + '\">' +\n          '<span class=\"libro-titulo-bloque\">' +\n            '<p class=\"libro-titulo\">' + escapeHtml(libro.titulo) + '<\/p>' +\n            etiquetasPrestado(libro) +\n            '<p class=\"libro-autor\">' + escapeHtml(libro.autor || \"Autor no consignado\") + '<\/p>' +\n          '<\/span>' +\n          '<svg class=\"libro-flecha\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">' +\n            '<path d=\"M9 6l6 6-6 6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/>' +\n          '<\/svg>' +\n        '<\/button>' +\n      '<\/article>'\n    );\n  }\n\n  function mostrarDetalle(indice){\n    var libro = listaActual[indice];\n    if (!libro) return;\n    scrollLista = window.pageYOffset;\n    elEstado.textContent = \"\";\n\n    var filas = camposLibro(libro).map(function(c){\n      return '<dt>' + c[0] + '<\/dt><dd>' + escapeHtml(c[1]) + '<\/dd>';\n    }).join(\"\");\n\n    var botonEditar = (tokenSesion ? permisosActuales.catalogo : false)\n      ? '<button type=\"button\" class=\"btn-editar\" id=\"btnEditar\">Editar libro<\/button>'\n      : '';\n    var botonEliminar = (tokenSesion ? permisosActuales.catalogo : false)\n      ? '<button type=\"button\" class=\"btn-eliminar\" id=\"btnEliminar\">Eliminar<\/button>'\n      : '';\n\n    elResultados.classList.add(\"modo-detalle\");\n    elResultados.innerHTML =\n      '<div class=\"barra-acciones\">' +\n        '<button type=\"button\" class=\"btn-volver\" id=\"btnVolver\">\\u2190 Volver a los resultados<\/button>' +\n        '<span class=\"grupo\">' + botonEditar + botonEliminar +\n          '<button type=\"button\" class=\"btn-imprimir\" id=\"btnImprimir\">Imprimir<\/button>' +\n        '<\/span>' +\n      '<\/div>' +\n      '<article class=\"libro-ampliado\">' +\n        '<h2 class=\"ampliado-titulo\">' + escapeHtml(libro.titulo) + '<\/h2>' +\n        etiquetasPrestado(libro) +\n        '<dl class=\"ficha\">' + filas + '<\/dl>' +\n      '<\/article>';\n\n    document.getElementById(\"btnVolver\").addEventListener(\"click\", function(){\n      elCampo.value = ultimaConsulta;\n      buscar(ultimaConsulta);\n      var tarjeta = elResultados.querySelectorAll(\".libro-cabecera\")[indice];\n      if (tarjeta) tarjeta.focus({ preventScroll:true });\n      window.scrollTo(0, scrollLista);\n    });\n    document.getElementById(\"btnImprimir\").addEventListener(\"click\", function(){\n      descargarImagen(libro);\n    });\n    var btnEditar = document.getElementById(\"btnEditar\");\n    if (btnEditar) btnEditar.addEventListener(\"click\", function(){ abrirFormularioLibro(libro); });\n    var btnEliminar = document.getElementById(\"btnEliminar\");\n    if (btnEliminar) btnEliminar.addEventListener(\"click\", function(){ abrirConfirmarEliminar(libro); });\n\n    elResultados.scrollIntoView();\n    document.getElementById(\"btnVolver\").focus({ preventScroll:true });\n  }\n\n  \/\/ Dibuja la ficha en un canvas y la descarga como .jpg\n  function descargarImagen(libro){\n    var F_TIT = '700 34px Barlow, Calibri, sans-serif';\n    var F_ET  = '600 19px Barlow, Calibri, sans-serif';\n    var F_VAL = '22px Calibri, Candara, \"Segoe UI\", Arial, sans-serif';\n    var W = 900, pad = 48, colEt = 210;\n    var anchoTotal = W - pad * 2, anchoVal = anchoTotal - colEt;\n\n    function lineas(ctx, texto, fuente, max){\n      ctx.font = fuente;\n      var res = [], actual = \"\";\n      texto.split(\/\\s+\/).forEach(function(palabra){\n        var prueba = actual ? actual + \" \" + palabra : palabra;\n        var ancho = ctx.measureText(prueba).width;\n        var seExcede = ancho !== Math.min(ancho, max);\n        if (actual === \"\") { actual = prueba; }\n        else if (seExcede) { res.push(actual); actual = palabra; }\n        else { actual = prueba; }\n      });\n      if (actual) res.push(actual);\n      return res;\n    }\n\n    var fuentesListas = document.fonts\n      ? Promise.all([document.fonts.load(\"700 34px Barlow\"), document.fonts.load(\"600 19px Barlow\")])\n      : Promise.resolve();\n\n    fuentesListas.catch(function(){}).then(function(){\n      var medir = document.createElement(\"canvas\").getContext(\"2d\");\n      var tit = lineas(medir, libro.titulo, F_TIT, anchoTotal);\n      var filas = camposLibro(libro).map(function(c){\n        return { et:c[0], lin:lineas(medir, c[1], F_VAL, anchoVal) };\n      });\n\n      var yTit = 108;\n      var yCampos = yTit + tit.length * 44 + 34;\n      var alto = yCampos + filas.reduce(function(suma, f){ return suma + f.lin.length * 30 + 18; }, 0) + 28;\n\n      var cv = document.createElement(\"canvas\");\n      cv.width = W; cv.height = alto;\n      var g = cv.getContext(\"2d\");\n      g.textBaseline = \"top\";\n      g.fillStyle = \"#fff\"; g.fillRect(0, 0, W, alto);\n\n      var deg = g.createLinearGradient(0, 0, W, 0);\n      deg.addColorStop(0, \"#22409b\"); deg.addColorStop(1, \"#6d7fd6\");\n      g.fillStyle = deg; g.fillRect(0, 0, W, 72);\n      [\"#8bc7e8\", \"#9ccfa4\", \"#f3bd82\"].forEach(function(color, i){\n        g.fillStyle = color; g.fillRect(i * W \/ 3, 72, W \/ 3, 8);\n      });\n      g.fillStyle = \"#fff\"; g.font = F_ET;\n      g.fillText(\"Biblioteca \\u00b7 IFDC Villa Regina\", pad, 26);\n\n      g.fillStyle = \"#22409b\"; g.font = F_TIT;\n      tit.forEach(function(linea, i){ g.fillText(linea, pad, yTit + i * 44); });\n\n      g.fillStyle = \"#e4e2db\"; g.fillRect(pad, yCampos - 16, anchoTotal, 2);\n\n      var y = yCampos;\n      filas.forEach(function(f){\n        g.fillStyle = \"#5b6072\"; g.font = F_ET;\n        g.fillText(f.et, pad, y + 3);\n        g.fillStyle = \"#23283b\"; g.font = F_VAL;\n        f.lin.forEach(function(linea, i){ g.fillText(linea, pad + colEt, y + i * 30); });\n        y += f.lin.length * 30 + 18;\n      });\n\n      cv.toBlob(function(blob){\n        var enlace = document.createElement(\"a\");\n        var nombre = normalizar(libro.titulo).replace(\/[^a-z0-9]+\/g, \"-\").replace(\/^-|-$\/g, \"\").substring(0, 40);\n        enlace.href = URL.createObjectURL(blob);\n        enlace.download = \"ficha-\" + (nombre || \"libro\") + \".jpg\";\n        document.body.appendChild(enlace);\n        enlace.click();\n        document.body.removeChild(enlace);\n        setTimeout(function(){ URL.revokeObjectURL(enlace.href); }, 1500);\n      }, \"image\/jpeg\", 0.92);\n    });\n  }\n\n  \/\/ ============================================================\n  \/\/ Modo bibliotecario: cerrar sesi\u00f3n y actualizar datos\n  \/\/ ============================================================\n\n  var elToast = document.getElementById(\"toast\");\n  var timerToast = null;\n  function mostrarToast(texto, esError){\n    elToast.textContent = texto;\n    elToast.className = \"toast\" + (esError ? \" error\" : \"\");\n    elToast.hidden = false;\n    if (timerToast) clearTimeout(timerToast);\n    timerToast = setTimeout(function(){ elToast.hidden = true; }, esError ? 5000 : 2500);\n  }\n\n  \/\/ Deja todas las pesta\u00f1as como reci\u00e9n abiertas: sin textos escritos en los buscadores,\n  \/\/ sin fichas abiertas ni informes mostrados, y en la pesta\u00f1a Cat\u00e1logo.\n  function reiniciarVistas(){\n    [elModalLibro, elModalConfirmar, elModalSocio, elModalConfirmarSocio, elModalLogin].forEach(function(m){ m.hidden = true; });\n\n    elCampo.value = \"\";\n    elEstado.textContent = \"\";\n    ultimaConsulta = \"\";\n\n    socioActual = null;\n    socioEnEdicion = null;\n    ultimaConsultaSocios = \"\";\n    elListaSocios.classList.remove(\"modo-detalle\");\n    elCampoSocios.value = \"\";\n    elEstadoSocios.textContent = \"\";\n\n    informeActivo = null;\n    elBtnInfSocios.classList.remove(\"activo\");\n    elBtnInfTodos.classList.remove(\"activo\");\n    elEstadoInforme.textContent = \"\";\n    elResultadoInforme.innerHTML = '<div class=\"mensaje\">Eleg\u00ed un informe con los botones de arriba.<\/div>';\n\n    elCampoHistorial.value = \"\";\n    elEstadoHistorial.textContent = \"\";\n    elResultadoHistorial.innerHTML = '<div class=\"mensaje\">Escrib\u00ed un libro o un socio para ver su historial de pr\u00e9stamos.<\/div>';\n\n    elTextoMensaje.value = \"\";\n    actualizarContadorMensaje();\n    elAvisoMensaje.textContent = \"\";\n    mensajes = [];\n    hayMasMensajes = false;\n    cargandoMensajes = false;\n    elListaMensajes.innerHTML = \"\";\n    elBtnMasMensajes.hidden = true;\n\n    irAPestana(\"catalogo\");\n    window.scrollTo(0, 0);\n  }\n\n  \/\/ Oculta los botones que el perfil no puede usar (el Apps Script adem\u00e1s rechaza esas acciones).\n  function aplicarPermisos(){\n    var c = document.body.classList;\n    if (!tokenSesion){\n      [\"sin-catalogo\", \"sin-socios\", \"sin-eliminar-socios\", \"sin-prestamos\", \"sin-mensajes\"].forEach(function(x){ c.remove(x); });\n      return;\n    }\n    c.toggle(\"sin-catalogo\", !permisosActuales.catalogo);\n    c.toggle(\"sin-socios\", !permisosActuales.socios);\n    c.toggle(\"sin-eliminar-socios\", !permisosActuales.eliminarSocios);\n    c.toggle(\"sin-prestamos\", !permisosActuales.prestamos);\n    c.toggle(\"sin-mensajes\", !permisosActuales.mensajes);\n  }\n\n  function sesionVencida(){\n    if (!tokenSesion) return;\n    cerrarSesion(true);\n    mostrarToast(\"Tu sesi\u00f3n venci\u00f3. Ingres\u00e1 de nuevo.\", true);\n  }\n\n  function cerrarSesion(sinAviso){\n    \/\/ Se le avisa al servicio para que invalide la credencial.\n    if (!sinAviso) llamarServicio({ accion: \"cerrarSesion\" }).catch(function(){});\n    tokenSesion = null;\n    usuarioActual = \"\";\n    perfilActual = \"\";\n    permisosActuales = { catalogo: false, socios: false, eliminarSocios: false, prestamos: false, mensajes: false };\n    aplicarPermisos();\n    elBarraBibliotecario.querySelector(\".etiqueta\").textContent = \"Modo bibliotecario activo\";\n    document.body.classList.remove(\"modo-bibliotecario\");\n    elBtnBibliotecarios.textContent = \"Acceso bibliotecarios\";\n    elBtnBibliotecarios.classList.remove(\"activo\");\n    elBarraBibliotecario.hidden = true;\n    elPestanas.hidden = true;\n\n    \/\/ Se borran de la memoria de la p\u00e1gina los datos privados (socios, pr\u00e9stamos, historial).\n    socios = [];\n    sociosCargados = false;\n    historial = [];\n    historialCargado = false;\n    elListaSocios.innerHTML = \"\";\n    reiniciarVistas();\n\n    if (!MOSTRAR_PRESTADOS_AL_PUBLICO) prestadosPorInv = {};\n    if (libros.length !== 0) buscar(\"\");   \/\/ vuelve a dibujar sin los botones de edici\u00f3n\n  }\n\n  \/\/ Vuelve a leer todo, como si se acabara de entrar al modo bibliotecario.\n  var refrescando = false;\n  function refrescarTodo(){\n    if (refrescando) return;\n    if (!tokenSesion) return;\n    refrescando = true;\n    var elBtn = document.getElementById(\"btnRefrescar\");\n    elBtn.classList.add(\"girando\");\n    elBtn.disabled = true;\n    var inicio = Date.now();\n    var pendientes = 2;          \/\/ cat\u00e1logo y socios\n    var fallo = \"\";\n\n    function uno(mensajeError){\n      if (mensajeError) fallo = mensajeError;\n      pendientes -= 1;\n      if (pendientes !== 0) return;\n      var resto = Math.max(0, 800 - (Date.now() - inicio));\n      setTimeout(function(){\n        refrescando = false;\n        elBtn.classList.remove(\"girando\");\n        elBtn.disabled = false;\n        if (tokenSesion) mostrarToast(fallo ? \"No se pudo actualizar: \" + fallo : \"Datos actualizados.\", !!fallo);\n      }, resto);\n    }\n\n    \/\/ Primero se borra todo lo escrito y se vuelve al Cat\u00e1logo; despu\u00e9s se vuelven a leer los datos.\n    reiniciarVistas();\n    historialCargado = false;\n\n    cargarCatalogo(function(){ buscar(\"\"); uno(\"\"); },\n                   function(){ uno(ultimoError || \"no se pudo leer el cat\u00e1logo.\"); });\n    cargarPrestados();\n    cargarSocios(function(){\n      if (tokenSesion) buscarSocios(\"\");\n      uno(\"\");\n    }, function(mensaje){ uno(mensaje); });\n  }\n  document.getElementById(\"btnRefrescar\").addEventListener(\"click\", refrescarTodo);\n\n  \/\/ ============================================================\n  \/\/ Modo bibliotecario: login\n  \/\/ ============================================================\n\n  elBtnBibliotecarios.addEventListener(\"click\", function(){\n    if (tokenSesion){\n      \/\/ Ya est\u00e1 logueado: el mismo bot\u00f3n cierra la sesi\u00f3n.\n      cerrarSesion();\n      return;\n    }\n    elAvisoLogin.textContent = \"\";\n    elUsuarioLogin.value = \"\";\n    elClaveLogin.value = \"\";\n    elModalLogin.hidden = false;\n    elUsuarioLogin.focus();\n  });\n\n  elBtnCancelarLogin.addEventListener(\"click\", function(){ elModalLogin.hidden = true; });\n\n  elFormLogin.addEventListener(\"submit\", function(e){\n    e.preventDefault();\n    var elBtnIngresar = document.getElementById(\"btnIngresarLogin\");\n\n    elBtnIngresar.disabled = true;\n    elAvisoLogin.className = \"aviso-form\";\n    elAvisoLogin.textContent = \"Verificando...\";\n\n    llamarServicio({ accion: \"ingresar\", usuario: elUsuarioLogin.value.trim(), contrasena: elClaveLogin.value })\n      .then(function(res){\n        elBtnIngresar.disabled = false;\n        if (!res.ok){\n          elAvisoLogin.className = \"aviso-form error\";\n          elAvisoLogin.textContent = res.mensaje || \"Usuario o contrase\u00f1a incorrectos.\";\n          elClaveLogin.value = \"\";\n          return;\n        }\n        tokenSesion = res.token;\n        usuarioActual = res.usuario;\n        perfilActual = res.perfil || \"\";\n        var pm = res.permisos ? res.permisos : {};\n        permisosActuales = { catalogo: !!pm.catalogo, socios: !!pm.socios, eliminarSocios: !!pm.eliminarSocios,\n                             prestamos: !!pm.prestamos, mensajes: !!pm.mensajes };\n        elClaveLogin.value = \"\";\n        elModalLogin.hidden = true;\n        elBtnBibliotecarios.textContent = res.usuario + \" \\u2713 \\u00b7 Cerrar sesi\u00f3n\";\n        elBtnBibliotecarios.title = \"Cerrar sesi\u00f3n\";\n        elBtnBibliotecarios.classList.add(\"activo\");\n        elBarraBibliotecario.hidden = false;\n        document.body.classList.add(\"modo-bibliotecario\");\n        aplicarPermisos();\n        elBarraBibliotecario.querySelector(\".etiqueta\").textContent = \"Modo bibliotecario activo\" + (perfilActual ? \" \\u00b7 Perfil: \" + perfilActual : \"\");\n        activarPestanas();\n        cargarPrestados();\n      })\n      .catch(function(err){\n        elBtnIngresar.disabled = false;\n        elAvisoLogin.className = \"aviso-form error\";\n        elAvisoLogin.textContent = msgErr(err, \"Error de conexi\u00f3n con el servicio.\");\n        console.error(err);\n      });\n  });\n\n  \/\/ ============================================================\n  \/\/ Modo bibliotecario: alta \/ edici\u00f3n de libro\n  \/\/ ============================================================\n\n  elBtnAgregarLibro.addEventListener(\"click\", function(){ abrirFormularioLibro(null); });\n  elBtnCancelarLibro.addEventListener(\"click\", function(){ elModalLibro.hidden = true; });\n\n  function abrirFormularioLibro(libro){\n    filaEnEdicion = libro ? libro.fila : null;\n    elTituloModalLibro.textContent = libro ? \"Editar libro\" : \"Cargar libro nuevo\";\n    elDescModalLibro.textContent = libro\n      ? \"Se actualiza esta fila de la planilla (fila \" + libro.fila + \").\"\n      : \"Se agrega debajo de la \u00faltima fila con datos de la planilla.\";\n    elAvisoLibro.textContent = \"\";\n    elAvisoLibro.className = \"aviso-form\";\n\n    document.getElementById(\"fTitulo\").value       = libro ? libro.titulo      : \"\";\n    document.getElementById(\"fAutor\").value        = libro ? libro.autor       : \"\";\n    document.getElementById(\"fAutoresSec\").value   = libro ? libro.autoresSec  : \"\";\n    document.getElementById(\"fEditorial\").value    = libro ? libro.editorial   : \"\";\n    document.getElementById(\"fAnio\").value         = libro ? libro.anio        : \"\";\n    document.getElementById(\"fIsbn\").value         = libro ? libro.isbn        : \"\";\n    document.getElementById(\"fSeccion\").value      = libro ? libro.seccion     : \"\";\n    document.getElementById(\"fSignatura\").value    = libro ? libro.signatura   : \"\";\n    document.getElementById(\"fInventario\").value   = libro ? libro.inventario  : \"\";\n    document.getElementById(\"fCantidad\").value     = libro ? libro.cantidad    : \"\";\n    document.getElementById(\"fEstado\").value       = libro ? libro.estado      : \"\";\n    document.getElementById(\"fDescriptores\").value = libro ? libro.descriptores: \"\";\n    document.getElementById(\"fObserv\").value       = libro ? libro.observ      : \"\";\n\n    elModalLibro.hidden = false;\n    document.getElementById(\"fTitulo\").focus();\n  }\n\n  elFormLibro.addEventListener(\"submit\", function(e){\n    e.preventDefault();\n\n    var datosLibro = {\n      titulo:       document.getElementById(\"fTitulo\").value.trim(),\n      autor:        document.getElementById(\"fAutor\").value.trim(),\n      autoresSec:   document.getElementById(\"fAutoresSec\").value.trim(),\n      editorial:    document.getElementById(\"fEditorial\").value.trim(),\n      anio:         document.getElementById(\"fAnio\").value.trim(),\n      isbn:         document.getElementById(\"fIsbn\").value.trim(),\n      seccion:      document.getElementById(\"fSeccion\").value.trim(),\n      signatura:    document.getElementById(\"fSignatura\").value.trim(),\n      inventario:   document.getElementById(\"fInventario\").value.trim(),\n      cantidad:     document.getElementById(\"fCantidad\").value.trim(),\n      estado:       document.getElementById(\"fEstado\").value.trim(),\n      descriptores: document.getElementById(\"fDescriptores\").value.trim(),\n      observ:       document.getElementById(\"fObserv\").value.trim()\n    };\n    if (filaEnEdicion) datosLibro.editarFila = filaEnEdicion;\n\n    \/\/ Campos obligatorios (tambi\u00e9n controla los que solo tienen espacios).\n    var faltanLibro = [[\"titulo\", \"T\u00edtulo\"], [\"autor\", \"Autor principal\"], [\"signatura\", \"Signatura(s)\"],\n                       [\"inventario\", \"N\u00b0 de inventario\"], [\"cantidad\", \"Cantidad de ejemplares\"]]\n      .filter(function(c){ return datosLibro[c[0]] === \"\"; })\n      .map(function(c){ return c[1]; });\n    if (faltanLibro.length !== 0){\n      elAvisoLibro.className = \"aviso-form error\";\n      elAvisoLibro.textContent = \"Complet\u00e1 los campos obligatorios: \" + faltanLibro.join(\", \") + \".\";\n      return;\n    }\n\n    elBtnGuardarLibro.disabled = true;\n    elAvisoLibro.className = \"aviso-form\";\n    elAvisoLibro.textContent = \"Guardando...\";\n\n    fetch(URL_APPS_SCRIPT, {\n      method: \"POST\",\n      \/\/ text\/plain evita que el navegador dispare un preflight OPTIONS,\n      \/\/ que Apps Script no responde correctamente.\n      headers: { \"Content-Type\": \"text\/plain;charset=utf-8\" },\n      body: JSON.stringify({\n        token: tokenSesion,\n        accion: \"guardar\",\n        libro: datosLibro\n      })\n    })\n      .then(function(resp){\n        return resp.text().then(function(texto){\n          var res;\n          try { res = JSON.parse(texto); }\n          catch (err){\n            \/\/ La respuesta no fue JSON: normalmente pasa cuando la\n            \/\/ implementaci\u00f3n del Apps Script no est\u00e1 publicada con acceso\n            \/\/ \"Cualquiera\", o no se cre\u00f3 una \"Nueva versi\u00f3n\" tras editarlo.\n            throw new Error(\n              \"El servicio no devolvi\u00f3 una respuesta v\u00e1lida (HTTP \" + resp.status + \"). \" +\n              \"Revis\u00e1 que la implementaci\u00f3n est\u00e9 publicada con acceso \\u201cCualquiera\\u201d \" +\n              \"y que sea la \\u00faltima versi\u00f3n.\"\n            );\n          }\n          return res;\n        });\n      })\n      .then(function(res){\n        elBtnGuardarLibro.disabled = false;\n        if (!res.ok){\n          elAvisoLibro.className = \"aviso-form error\";\n          elAvisoLibro.textContent = res.mensaje || \"No se pudo guardar el libro.\";\n          if (res.sesionVencida) sesionVencida();\n          return;\n        }\n        elAvisoLibro.className = \"aviso-form ok\";\n        elAvisoLibro.textContent = res.mensaje || \"Guardado.\";\n        cargarCatalogo(function(){\n          elModalLibro.hidden = true;\n          buscar(ultimaConsulta || \"\");\n        });\n      })\n      .catch(function(err){\n        elBtnGuardarLibro.disabled = false;\n        elAvisoLibro.className = \"aviso-form error\";\n        elAvisoLibro.textContent = msgErr(err, \"Error de conexi\u00f3n con el servicio. Prob\u00e1 de nuevo.\");\n        console.error(err);\n      });\n  });\n\n  \/\/ ============================================================\n  \/\/ Modo bibliotecario: eliminar libro (con confirmaci\u00f3n)\n  \/\/ ============================================================\n\n  function abrirConfirmarEliminar(libro){\n    libroAEliminar = libro;\n    elDescConfirmar.textContent =\n      '\\u00bfSegur\\u00f3 que quer\\u00e9s eliminar \"' + libro.titulo + '\" del cat\\u00e1logo? Esta acci\\u00f3n no se puede deshacer.';\n    elAvisoConfirmar.className = \"aviso-form\";\n    elAvisoConfirmar.textContent = \"\";\n    elModalConfirmar.hidden = false;\n  }\n\n  elBtnCancelarEliminar.addEventListener(\"click\", function(){\n    elModalConfirmar.hidden = true;\n    libroAEliminar = null;\n  });\n\n  elBtnConfirmarEliminar.addEventListener(\"click\", function(){\n    if (!libroAEliminar) return;\n\n    elBtnConfirmarEliminar.disabled = true;\n    elAvisoConfirmar.className = \"aviso-form\";\n    elAvisoConfirmar.textContent = \"Eliminando...\";\n\n    fetch(URL_APPS_SCRIPT, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"text\/plain;charset=utf-8\" },\n      body: JSON.stringify({\n        token: tokenSesion,\n        accion: \"eliminar\",\n        fila: libroAEliminar.fila\n      })\n    })\n      .then(function(resp){\n        return resp.text().then(function(texto){\n          var res;\n          try { res = JSON.parse(texto); }\n          catch (err){\n            throw new Error(\"El servicio no devolvi\u00f3 una respuesta v\u00e1lida (HTTP \" + resp.status + \").\");\n          }\n          return res;\n        });\n      })\n      .then(function(res){\n        elBtnConfirmarEliminar.disabled = false;\n        if (res.sesionVencida) sesionVencida();\n        if (!res.ok){\n          elAvisoConfirmar.className = \"aviso-form error\";\n          elAvisoConfirmar.textContent = res.mensaje || \"No se pudo eliminar el libro.\";\n          return;\n        }\n        elModalConfirmar.hidden = true;\n        libroAEliminar = null;\n        cargarCatalogo(function(){ buscar(ultimaConsulta || \"\"); });\n      })\n      .catch(function(err){\n        elBtnConfirmarEliminar.disabled = false;\n        elAvisoConfirmar.className = \"aviso-form error\";\n        elAvisoConfirmar.textContent = msgErr(err, \"Error de conexi\u00f3n con el servicio.\");\n        console.error(err);\n      });\n  });\n\n  elForm.addEventListener(\"submit\", function(e){\n    e.preventDefault();\n    buscar(elCampo.value);\n  });\n\n  elCampo.addEventListener(\"input\", function(){\n    if (elCampo.value.trim() === \"\") buscar(\"\");\n  });\n\n  cargarCatalogo();\n  cargarPrestados();\n  \/\/ Si despu\u00e9s de 20 segundos sigue cargando, se muestra el aviso en vez de girar sin fin.\n  setTimeout(function(){\n    if (elResultados.querySelector(\".spinner\")){ huboError = true; mostrarError(); }\n  }, 20000);\n\n  \/\/ ============================================================\n  \/\/ Modo bibliotecario: pesta\u00f1as Cat\u00e1logo \/ Pr\u00e9stamos\n  \/\/ ============================================================\n  var elPestanas = document.getElementById(\"pestanasBib\");\n  var elTabCatalogo = document.getElementById(\"tabCatalogo\");\n  var elTabPrestamos = document.getElementById(\"tabPrestamos\");\n  var elVistaCatalogo = document.getElementById(\"vistaCatalogo\");\n  var elVistaPrestamos = document.getElementById(\"vistaPrestamos\");\n\n  function activarPestanas(){ elPestanas.hidden = false; }\n\n  var elVistaInforme = document.getElementById(\"vistaInforme\");\n  var elTabInforme = document.getElementById(\"tabInforme\");\n\n  var elVistaHistorial = document.getElementById(\"vistaHistorial\");\n  var elTabHistorial = document.getElementById(\"tabHistorial\");\n\n  function irAPestana(nombre){\n    var prestamos = nombre === \"prestamos\";\n    var informe = nombre === \"informe\";\n    var historia = nombre === \"historial\";\n    elVistaCatalogo.hidden = nombre !== \"catalogo\";\n    elVistaPrestamos.hidden = !prestamos;\n    elVistaInforme.hidden = !informe;\n    elVistaHistorial.hidden = !historia;\n    document.getElementById(\"vistaComunicacion\").hidden = nombre !== \"comunicacion\";\n    document.getElementById(\"tabComunicacion\").classList.toggle(\"activa\", nombre === \"comunicacion\");\n    elTabCatalogo.classList.toggle(\"activa\", nombre === \"catalogo\");\n    elTabPrestamos.classList.toggle(\"activa\", prestamos);\n    elTabInforme.classList.toggle(\"activa\", informe);\n    elTabHistorial.classList.toggle(\"activa\", historia);\n    if (prestamos ? !sociosCargados : false) cargarSocios();\n    if (historia) cargarHistorial();\n    if (nombre === \"comunicacion\") cargarMensajes(true);\n  }\n  elTabInforme.addEventListener(\"click\", function(){ irAPestana(\"informe\"); });\n  elTabHistorial.addEventListener(\"click\", function(){ irAPestana(\"historial\"); });\n  document.getElementById(\"tabComunicacion\").addEventListener(\"click\", function(){ irAPestana(\"comunicacion\"); });\n  elTabCatalogo.addEventListener(\"click\", function(){ irAPestana(\"catalogo\"); });\n  elTabPrestamos.addEventListener(\"click\", function(){ irAPestana(\"prestamos\"); });\n\n  \/\/ ============================================================\n  \/\/ Pr\u00e9stamos: socios (hoja \"Socios\", columnas A-AA)\n  \/\/ A DNI | B Apellido | C Nombre | D Tel\u00e9fono | E Mail | F Carrera | G Usuario\n  \/\/ H-AA: N\u00b0 Inventario \/ Fecha de los pr\u00e9stamos 1 a 10\n  \/\/ Se lee y escribe a trav\u00e9s del Apps Script (con clave), as\u00ed los datos\n  \/\/ personales no quedan expuestos en una lectura p\u00fablica de la planilla.\n  \/\/ ============================================================\n\n  var elListaSocios = document.getElementById(\"listaSocios\");\n  var elEstadoSocios = document.getElementById(\"estadoSocios\");\n  var elCampoSocios = document.getElementById(\"campoSocios\");\n  var elModalSocio = document.getElementById(\"modalSocio\");\n  var elAvisoSocio = document.getElementById(\"avisoSocio\");\n  var elBtnGuardarSocio = document.getElementById(\"btnGuardarSocio\");\n  var elModalConfirmarSocio = document.getElementById(\"modalConfirmarSocio\");\n  var elAvisoConfirmarSocio = document.getElementById(\"avisoConfirmarSocio\");\n  var elBtnConfirmarEliminarSocio = document.getElementById(\"btnConfirmarEliminarSocio\");\n\n  var socios = [];\n  var sociosCargados = false;\n  var ultimaConsultaSocios = \"\";\n  var socioActual = null;\n  var socioEnEdicion = null;\n\n  function llamarServicio(datos){\n    datos.token = tokenSesion;\n    return fetch(URL_APPS_SCRIPT, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"text\/plain;charset=utf-8\" },\n      body: JSON.stringify(datos)\n    }).then(function(resp){\n      return resp.text().then(function(texto){\n        var r;\n        try { r = JSON.parse(texto); }\n        catch (err){\n          throw new Error(\"El servicio no devolvi\u00f3 una respuesta v\u00e1lida (HTTP \" + resp.status +\n            \"). Revis\u00e1 que el Apps Script est\u00e9 publicado con la \u00faltima versi\u00f3n.\");\n        }\n        if (r.sesionVencida) sesionVencida();\n        return r;\n      });\n    });\n  }\n\n  function hoyTexto(){\n    var d = new Date();\n    return (\"0\" + d.getDate()).slice(-2) + \"\/\" + (\"0\" + (d.getMonth() + 1)).slice(-2) + \"\/\" + d.getFullYear();\n  }\n\n  function cargarSocios(callback, alError){\n    if (!alError) elListaSocios.innerHTML = '<div class=\"mensaje\"><div class=\"spinner\"><\/div>Cargando socios\u2026<\/div>';\n    llamarServicio({ accion: \"listarSocios\" })\n      .then(function(res){\n        if (!res.ok) throw new Error(res.mensaje || \"No se pudo leer la hoja Socios.\");\n        var rn = res.renovaciones ? res.renovaciones : {};\n        socios = res.socios.map(function(s){\n          var c = s.celdas.map(function(x){ return String(x == null ? \"\" : x).trim(); });\n          var prest = [];\n          for (var i = 0; i !== 10; i++){\n            var invI = c[7 + i * 2];\n            var renI = invI ? (Object.prototype.hasOwnProperty.call(rn, invI) ? rn[invI] : []) : [];\n            prest.push({ inv: invI, fecha: c[8 + i * 2], renov: renI });\n          }\n          return { fila: s.fila, dni: c[0], apellido: c[1], nombre: c[2], telefono: c[3],\n                   mail: c[4], carrera: c[5], usuario: c[6], prestamos: prest };\n        });\n        sociosCargados = true;\n        prestadosDesdeSocios();\n        if (callback) callback(); else buscarSocios(ultimaConsultaSocios);\n      })\n      .catch(function(err){\n        sociosCargados = false;\n        if (alError){ alError(msgErr(err, \"Error de conexi\u00f3n.\")); return; }\n        elListaSocios.innerHTML = '<div class=\"mensaje\">' + escapeHtml(msgErr(err, \"Error de conexi\u00f3n.\")) + '<\/div>';\n      });\n  }\n\n  function cantidadPrestamos(s){\n    return s.prestamos.filter(function(p){ return p.inv; }).length;\n  }\n\n  \/\/ \u00bfEl socio tiene prestado ese N\u00b0 de inventario (columnas Pr\u00e9stamo 1 a 10)?\n  function tieneInventario(s, p){\n    for (var i = 0; i !== s.prestamos.length; i++){\n      if (s.prestamos[i].inv) { if (normalizar(s.prestamos[i].inv) === p) return true; }\n    }\n    return false;\n  }\n\n  function coincideSocio(s, palabras){\n    var campos = normalizar([s.dni, s.apellido, s.nombre, s.telefono, s.mail, s.carrera, s.usuario].join(\" \"));\n    return palabras.every(function(p){ return tieneInventario(s, p) ? true : campos.indexOf(p) !== -1; });\n  }\n\n  function buscarSocios(consulta){\n    elListaSocios.classList.remove(\"modo-detalle\");\n    ultimaConsultaSocios = consulta;\n    socioActual = null;\n    var texto = consulta.trim();\n    if (!texto){\n      elEstadoSocios.textContent = \"\";\n      elListaSocios.innerHTML = '<div class=\"mensaje\">Hay <strong>' + socios.length +\n        '<\/strong> socios registrados. Busc\u00e1 por DNI, apellido, nombre o N\u00b0 de inventario.<\/div>';\n      return;\n    }\n    var palabras = normalizar(texto).split(\/\\s+\/).filter(Boolean).map(function(p){\n      return \/^[\\d.]+$\/.test(p) ? p.replace(\/\\.\/g, \"\") : p; \/\/ permite 11.222.333\n    });\n    var encontrados = socios.filter(function(s){ return coincideSocio(s, palabras); });\n    var lbBuscado = palabras.length === 1 ? libroPorInventario(palabras[0]) : null;\n    var cajaLibro = lbBuscado ? htmlLibroInventario(lbBuscado, palabras[0]) : \"\";\n    elEstadoSocios.textContent = encontrados.length\n      ? encontrados.length + (encontrados.length === 1 ? \" socio\" : \" socios\") + \" para \\u201c\" + texto + \"\\u201d\"\n      : \"\";\n    if (!encontrados.length){\n      if (lbBuscado){ elListaSocios.innerHTML = cajaLibro; return; }\n      elListaSocios.innerHTML = '<div class=\"mensaje\">No encontramos socios que coincidan con <strong>\\u201c' +\n        escapeHtml(texto) + '\\u201d<\/strong>. Pod\u00e9s cargarlo con el bot\u00f3n \\u201c+ Nuevo socio\\u201d.<\/div>';\n      return;\n    }\n    elListaSocios.innerHTML = cajaLibro + encontrados.map(function(s, i){\n      var n = cantidadPrestamos(s);\n      var extra = \"\";\n      s.prestamos.forEach(function(pr){\n        if (pr.inv ? palabras.indexOf(normalizar(pr.inv)) !== -1 : false){\n          var lb = libroPorInventario(pr.inv);\n          extra += '<p class=\"libro-autor\">N\u00b0 ' + escapeHtml(pr.inv) + \": \" + escapeHtml(lb ? lb.titulo : \"(no figura en el cat\u00e1logo)\") +\n                   (pr.fecha ? \" \\u00b7 desde \" + escapeHtml(pr.fecha) : \"\") + '<\/p>';\n        }\n      });\n      var sub = \"DNI \" + s.dni + (s.usuario ? \" \\u00b7 \" + s.usuario : \"\") +\n                (n ? \" \\u00b7 \" + n + (n === 1 ? \" pr\u00e9stamo\" : \" pr\u00e9stamos\") : \"\");\n      return '<article class=\"libro\"><button type=\"button\" class=\"libro-cabecera\" data-fila=\"' + s.fila + '\">' +\n        '<span class=\"libro-titulo-bloque\"><p class=\"libro-titulo\">' + escapeHtml(s.apellido + (s.nombre ? \", \" + s.nombre : \"\")) + '<\/p>' +\n        '<p class=\"libro-autor\">' + escapeHtml(sub) + '<\/p>' + extra + '<\/span>' +\n        '<svg class=\"libro-flecha\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><path d=\"M9 6l6 6-6 6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>' +\n        '<\/button><\/article>';\n    }).join(\"\");\n    Array.prototype.forEach.call(elListaSocios.querySelectorAll(\".libro-cabecera\"), function(b){\n      b.addEventListener(\"click\", function(){ abrirSocio(parseInt(b.getAttribute(\"data-fila\"), 10)); });\n    });\n  }\n\n  function socioPorFila(fila){\n    for (var i = 0; i !== socios.length; i++) if (socios[i].fila === fila) return socios[i];\n    return null;\n  }\n\n  \/\/ Busca el libro del cat\u00e1logo por N\u00b0 de inventario (devuelve el libro o null).\n  function libroPorInventario(inv){\n    inv = String(inv || \"\").trim();\n    if (!inv) return null;\n    for (var i = 0; i !== libros.length; i++){\n      \/\/ Un libro puede tener varios N\u00b0 de inventario separados por coma (ej: \"2151, 2152, 6520\").\n      var nums = String(libros[i].inventario).split(\/[,;\\n]+\/);\n      for (var k = 0; k !== nums.length; k++){\n        if (nums[k].trim() === inv) return libros[i];\n      }\n    }\n    return null;\n  }\n\n  function tituloPorInventario(inv){\n    inv = String(inv || \"\").trim();\n    if (!inv) return null;\n    var lb = libroPorInventario(inv);\n    return lb ? lb.titulo : false;\n  }\n\n  function fechaDePrestamo(s, inv){\n    for (var i = 0; i !== s.prestamos.length; i++){\n      if (s.prestamos[i].inv === inv) return s.prestamos[i].fecha;\n    }\n    return \"\";\n  }\n\n  function nombreSocio(s){\n    return s.apellido + (s.nombre ? \", \" + s.nombre : \"\");\n  }\n\n  \/\/ Ficha del libro que se muestra al buscar por N\u00b0 de inventario.\n  function htmlLibroInventario(lb, inv){\n    var filas = [[\"Autor\", lb.autor], [\"Editorial\", lb.editorial], [\"A\u00f1o\", lb.anio], [\"Secci\u00f3n\", lb.seccion],\n                 [\"Signatura\", lb.signatura], [\"ISBN\", lb.isbn], [\"N\u00b0 de inventario\", lb.inventario], [\"Estado\", lb.estado]]\n      .filter(function(c){ return c[1]; })\n      .map(function(c){ return \"<dt>\" + c[0] + \"<\/dt><dd>\" + escapeHtml(c[1]) + \"<\/dd>\"; }).join(\"\");\n    var prestados = socios.filter(function(s){ return tieneInventario(s, inv); });\n    var situacion = prestados.length\n      ? prestados.map(function(s){\n          var f = fechaDePrestamo(s, inv);\n          return \"Prestado a \" + escapeHtml(nombreSocio(s)) + \" (DNI \" + escapeHtml(s.dni) + \")\" + (f ? \", desde el \" + escapeHtml(f) : \"\");\n        }).join(\"<br>\")\n      : \"No figura prestado a ning\u00fan socio.\";\n    return '<section class=\"socio-ficha\" style=\"margin-bottom:14px\"><h2>' + escapeHtml(lb.titulo) + '<\/h2><dl>' + filas +\n      '<dt>Situaci\u00f3n<\/dt><dd><strong>' + situacion + '<\/strong><\/dd><\/dl><\/section>';\n  }\n\n  function pistaLibro(i){\n    var inv = elListaSocios.querySelector('.p-inv[data-i=\"' + i + '\"]').value;\n    var el = document.getElementById(\"pl\" + i);\n    var t = tituloPorInventario(inv);\n    el.className = \"prest-libro\" + (t === false ? \" aviso\" : \"\");\n    el.textContent = t === null ? \"\" : (t === false ? \"Ese N\u00b0 de inventario no figura en el cat\u00e1logo.\" : \"Libro: \" + t);\n  }\n\n  function abrirSocio(fila){\n    var s = socioPorFila(fila);\n    if (!s) return;\n    socioActual = s;\n    scrollLista = window.pageYOffset;\n    elEstadoSocios.textContent = \"\";\n    elListaSocios.classList.add(\"modo-detalle\");\n\n    var datos = [[\"DNI\", s.dni], [\"Tel\u00e9fono\", s.telefono], [\"Mail\", s.mail], [\"Carrera\", s.carrera], [\"Usuario\", s.usuario]]\n      .filter(function(c){ return c[1]; })\n      .map(function(c){ return \"<dt>\" + c[0] + \"<\/dt><dd>\" + escapeHtml(c[1]) + \"<\/dd>\"; }).join(\"\");\n\n    var filasPrest = s.prestamos.map(function(p, i){\n      return '<div class=\"prest-fila' + (p.renov.length !== 0 ? ' renovada' : '') + '\" id=\"pf' + i + '\"><span class=\"prest-n\">Pr\u00e9stamo ' + (i + 1) + '<\/span>' +\n        '<input type=\"text\" class=\"p-inv\" data-i=\"' + i + '\" placeholder=\"N\u00b0 Inventario\" aria-label=\"N\u00b0 Inventario - Pr\u00e9stamo ' + (i + 1) + '\" value=\"' + escapeHtml(p.inv) + '\">' +\n        '<input type=\"text\" class=\"p-fecha\" data-i=\"' + i + '\" placeholder=\"dd\/mm\/aaaa\" aria-label=\"Fecha - Pr\u00e9stamo ' + (i + 1) + '\" value=\"' + escapeHtml(p.fecha) + '\">' +\n        '<span class=\"prest-botones\"><button type=\"button\" class=\"btn-renovar\" data-i=\"' + i + '\">Renovar<\/button>' +\n        '<button type=\"button\" class=\"btn-devolver\" data-i=\"' + i + '\">Devolver<\/button><\/span>' +\n        '<p class=\"prest-libro\" id=\"pl' + i + '\"><\/p><p class=\"prest-estado\" id=\"pe' + i + '\"><\/p><\/div>';\n    }).join(\"\");\n\n    elListaSocios.innerHTML =\n      '<div class=\"barra-acciones\"><button type=\"button\" class=\"btn-volver\" id=\"btnVolverSocios\">\\u2190 Volver a los resultados<\/button>' +\n      '<div class=\"grupo\"><button type=\"button\" class=\"btn-guardar\" id=\"btnGuardarPrestamos\">Guardar<\/button>' +\n      '<button type=\"button\" class=\"btn-editar\" id=\"btnEditarSocio\">Editar datos<\/button>' +\n      '<button type=\"button\" class=\"btn-eliminar\" id=\"btnEliminarSocio\">Eliminar socio<\/button><\/div><\/div>' +\n      '<p class=\"aviso-form\" id=\"avisoPrestamos\" style=\"margin:0 0 12px\"><\/p>' +\n      '<section class=\"socio-ficha\"><h2>' + escapeHtml(s.apellido + (s.nombre ? \", \" + s.nombre : \"\")) + '<\/h2><dl>' + datos + '<\/dl>' +\n      '<h3>Pr\u00e9stamos<\/h3><p style=\"margin:0 0 6px;font-size:14px;color:var(--texto-suave)\">Al cargar un N\u00b0 de inventario sin fecha, se completa con la de hoy. \\u201cDevolver\\u201d registra la devoluci\u00f3n en el historial y \\u201cRenovar\\u201d extiende el plazo 5 d\u00edas. Si borr\u00e1s un campo y guard\u00e1s, se toma como una correcci\u00f3n (no queda en el historial).<\/p>' +\n      filasPrest +\n      '<\/section>';\n\n    window.scrollTo(0, 0);\n    document.getElementById(\"btnVolverSocios\").addEventListener(\"click\", function(){\n      buscarSocios(ultimaConsultaSocios);\n      window.scrollTo(0, scrollLista);\n    });\n    document.getElementById(\"btnEditarSocio\").addEventListener(\"click\", function(){ abrirFormularioSocio(s); });\n    document.getElementById(\"btnEliminarSocio\").addEventListener(\"click\", function(){ abrirConfirmarSocio(s); });\n    document.getElementById(\"btnGuardarPrestamos\").addEventListener(\"click\", guardarPrestamos);\n\n    s.prestamos.forEach(function(p, i){ pintarEstadoPrestamo(i); actualizarBotonesPrestamo(i); pistaLibro(i); });\n    if (!permisosActuales.prestamos){\n      Array.prototype.forEach.call(elListaSocios.querySelectorAll(\".p-inv, .p-fecha\"), function(x){ x.readOnly = true; });\n    }\n    Array.prototype.forEach.call(elListaSocios.querySelectorAll(\".p-inv\"), function(inp){\n      var i = inp.getAttribute(\"data-i\");\n      inp.addEventListener(\"input\", function(){ pistaLibro(i); actualizarBotonesPrestamo(i); });\n      inp.addEventListener(\"change\", function(){\n        var f = elListaSocios.querySelector('.p-fecha[data-i=\"' + i + '\"]');\n        if (inp.value.trim() ? !f.value.trim() : false) f.value = hoyTexto();\n        actualizarBotonesPrestamo(i);\n      });\n    });\n    Array.prototype.forEach.call(elListaSocios.querySelectorAll(\".p-fecha\"), function(inp){\n      var i = inp.getAttribute(\"data-i\");\n      inp.addEventListener(\"input\", function(){ actualizarBotonesPrestamo(i); });\n    });\n    Array.prototype.forEach.call(elListaSocios.querySelectorAll(\".btn-devolver\"), function(b){\n      b.addEventListener(\"click\", function(){ accionPrestamo(\"devolverPrestamo\", parseInt(b.getAttribute(\"data-i\"), 10), b); });\n    });\n    Array.prototype.forEach.call(elListaSocios.querySelectorAll(\".btn-renovar\"), function(b){\n      b.addEventListener(\"click\", function(){ accionPrestamo(\"renovarPrestamo\", parseInt(b.getAttribute(\"data-i\"), 10), b); });\n    });\n  }\n\n  function valorCampo(clase, i){\n    return elListaSocios.querySelector(clase + '[data-i=\"' + i + '\"]').value.trim();\n  }\n\n  \/\/ Los botones Devolver \/ Renovar solo sirven para pr\u00e9stamos ya guardados y sin cambios pendientes.\n  function actualizarBotonesPrestamo(i){\n    var p = socioActual.prestamos[i];\n    var fila = document.getElementById(\"pf\" + i);\n    fila.querySelector(\".prest-botones\").style.display = p.inv ? \"flex\" : \"none\";\n    var igual = p.inv ? (valorCampo(\".p-inv\", i) === p.inv ? valorCampo(\".p-fecha\", i) === p.fecha : false) : false;\n    var ayuda = igual ? \"\" : \"Guard\u00e1 los cambios antes de devolver o renovar\";\n    fila.querySelector(\".btn-devolver\").disabled = !igual;\n    fila.querySelector(\".btn-renovar\").disabled = !igual;\n    fila.querySelector(\".btn-devolver\").title = ayuda;\n    fila.querySelector(\".btn-renovar\").title = ayuda;\n  }\n\n  function pintarEstadoPrestamo(i){\n    var p = socioActual.prestamos[i];\n    var el = document.getElementById(\"pe\" + i);\n    if (!p.inv){ el.textContent = \"\"; el.className = \"prest-estado\"; return; }\n    var venc = vencimientoTexto(p);\n    var renovada = p.renov.length !== 0;\n    var vencido = estaVencidoPrestamo(p);\n    var texto = renovada\n      ? \"Renovado el \" + p.renov.join(\", \") + (venc ? \" \\u00b7 nuevo vencimiento: \" + venc : \"\")\n      : (venc ? \"Vence el \" + venc : \"\");\n    if (vencido) texto += \" \\u00b7 plazo vencido\";\n    el.textContent = texto;\n    el.className = \"prest-estado\" + (vencido ? \" venc\" : (renovada ? \" renov\" : \"\"));\n  }\n\n  \/\/ Devolver o renovar un pr\u00e9stamo guardado.\n  function accionPrestamo(accion, i, boton){\n    var s = socioActual;\n    var aviso = document.getElementById(\"avisoPrestamos\");\n    boton.disabled = true;\n    aviso.className = \"aviso-form\";\n    aviso.textContent = \"Guardando...\";\n    llamarServicio({ accion: accion, socio: { fila: s.fila, dni: s.dni }, indice: i })\n      .then(function(res){\n        if (!res.ok){\n          boton.disabled = false;\n          aviso.className = \"aviso-form error\";\n          aviso.textContent = res.mensaje || \"No se pudo completar la acci\u00f3n.\";\n          return;\n        }\n        cargarSocios(function(){\n          abrirSocio(s.fila);\n          var a = document.getElementById(\"avisoPrestamos\");\n          if (a){ a.className = \"aviso-form ok\"; a.textContent = res.mensaje; }\n        });\n      })\n      .catch(function(err){\n        boton.disabled = false;\n        aviso.className = \"aviso-form error\";\n        aviso.textContent = msgErr(err, \"Error de conexi\u00f3n con el servicio.\");\n      });\n  }\n\n  \/\/ Un libro no puede estar prestado a dos socios a la vez.\n  function socioConInventario(inv, filaExcluida){\n    var buscado = normalizar(inv);\n    for (var i = 0; i !== socios.length; i++){\n      if (socios[i].fila !== filaExcluida){ if (tieneInventario(socios[i], buscado)) return socios[i]; }\n    }\n    return null;\n  }\n\n  function validarPrestamos(s, lista){\n    var vistos = {};\n    for (var i = 0; i !== lista.length; i++){\n      var inv = lista[i][0];\n      if (inv){\n        if (vistos[\"i\" + inv]) return \"El N\u00b0 de inventario \" + inv + \" est\u00e1 cargado dos veces.\";\n        vistos[\"i\" + inv] = true;\n        if (inv !== s.prestamos[i].inv){\n          var otro = socioConInventario(inv, s.fila);\n          if (otro) return \"El libro N\u00b0 \" + inv + \" ya est\u00e1 prestado a \" + nombreSocio(otro) + \" (DNI \" + otro.dni + \"). Registr\u00e1 primero su devoluci\u00f3n.\";\n        }\n      }\n    }\n    return \"\";\n  }\n\n  function datosSocioBase(s){\n    return { fila: s.fila, dniOriginal: s.dni, dni: s.dni, apellido: s.apellido, nombre: s.nombre,\n             telefono: s.telefono, mail: s.mail, carrera: s.carrera, usuario: s.usuario };\n  }\n\n  function guardarPrestamos(){\n    var s = socioActual;\n    if (!s) return;\n    var aviso = document.getElementById(\"avisoPrestamos\");\n    var btn = document.getElementById(\"btnGuardarPrestamos\");\n    var datos = datosSocioBase(s);\n    datos.editarFila = s.fila;\n    datos.prestamos = [];\n    for (var i = 0; i !== 10; i++){\n      datos.prestamos.push([\n        elListaSocios.querySelector('.p-inv[data-i=\"' + i + '\"]').value.trim(),\n        elListaSocios.querySelector('.p-fecha[data-i=\"' + i + '\"]').value.trim()\n      ]);\n    }\n    var problema = validarPrestamos(s, datos.prestamos);\n    if (problema){\n      aviso.className = \"aviso-form error\";\n      aviso.textContent = problema;\n      return;\n    }\n    btn.disabled = true;\n    aviso.className = \"aviso-form\";\n    aviso.textContent = \"Guardando...\";\n    llamarServicio({ accion: \"guardarSocio\", socio: datos })\n      .then(function(res){\n        btn.disabled = false;\n        if (!res.ok){ aviso.className = \"aviso-form error\"; aviso.textContent = res.mensaje || \"No se pudo guardar.\"; return; }\n        cargarSocios(function(){\n          abrirSocio(s.fila);\n          var a = document.getElementById(\"avisoPrestamos\");\n          if (a){ a.className = \"aviso-form ok\"; a.textContent = \"Pr\u00e9stamos guardados.\"; }\n        });\n      })\n      .catch(function(err){\n        btn.disabled = false;\n        aviso.className = \"aviso-form error\";\n        aviso.textContent = msgErr(err, \"Error de conexi\u00f3n con el servicio.\");\n      });\n  }\n\n  \/\/ ---- Alta \/ edici\u00f3n de datos del socio ----\n  function abrirFormularioSocio(s){\n    socioEnEdicion = s || null;\n    document.getElementById(\"tituloModalSocio\").textContent = s ? \"Editar socio\" : \"Nuevo socio\";\n    document.getElementById(\"descModalSocio\").textContent = s\n      ? \"Se actualizan los datos de este socio (los pr\u00e9stamos no se modifican).\"\n      : \"Se agrega debajo de la \u00faltima fila con datos de la hoja Socios.\";\n    elAvisoSocio.className = \"aviso-form\";\n    elAvisoSocio.textContent = \"\";\n    var ids = { sDni: \"dni\", sApellido: \"apellido\", sNombre: \"nombre\", sTelefono: \"telefono\", sMail: \"mail\", sCarrera: \"carrera\", sUsuario: \"usuario\" };\n    Object.keys(ids).forEach(function(id){\n      var valor = s ? s[ids[id]] : \"\";\n      var campo = document.getElementById(id);\n      if (campo.tagName === \"SELECT\"){\n        \/\/ Si el socio tiene un valor que no est\u00e1 en la lista (cargado antes), se conserva para no perderlo.\n        Array.prototype.slice.call(campo.querySelectorAll(\"option[data-extra]\")).forEach(function(o){ campo.removeChild(o); });\n        var existe = false;\n        Array.prototype.forEach.call(campo.options, function(o){ if (o.value === valor) existe = true; });\n        if (valor ? !existe : false){\n          var extra = document.createElement(\"option\");\n          extra.value = valor;\n          extra.textContent = valor;\n          extra.setAttribute(\"data-extra\", \"1\");\n          campo.appendChild(extra);\n        }\n      }\n      campo.value = valor;\n    });\n    elModalSocio.hidden = false;\n    document.getElementById(\"sDni\").focus();\n  }\n\n  document.getElementById(\"btnNuevoSocio\").addEventListener(\"click\", function(){ abrirFormularioSocio(null); });\n  document.getElementById(\"btnCancelarSocio\").addEventListener(\"click\", function(){ elModalSocio.hidden = true; });\n\n  document.getElementById(\"formSocio\").addEventListener(\"submit\", function(e){\n    e.preventDefault();\n    var v = function(id){ return document.getElementById(id).value.trim(); };\n    var datos = { dni: v(\"sDni\").replace(\/\\.\/g, \"\"), apellido: v(\"sApellido\"), nombre: v(\"sNombre\"),\n                  telefono: v(\"sTelefono\"), mail: v(\"sMail\"), carrera: v(\"sCarrera\"), usuario: v(\"sUsuario\") };\n\n    var faltanSocio = [[\"dni\", \"DNI\"], [\"apellido\", \"Apellido\"], [\"nombre\", \"Nombre\"], [\"mail\", \"Mail\"]]\n      .filter(function(c){ return datos[c[0]] === \"\"; })\n      .map(function(c){ return c[1]; });\n    if (faltanSocio.length !== 0){\n      elAvisoSocio.className = \"aviso-form error\";\n      elAvisoSocio.textContent = \"Complet\u00e1 los campos obligatorios: \" + faltanSocio.join(\", \") + \".\";\n      return;\n    }\n    if (!\/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\/.test(datos.mail)){\n      elAvisoSocio.className = \"aviso-form error\";\n      elAvisoSocio.textContent = \"El mail no parece v\u00e1lido (ejemplo: nombre@correo.com).\";\n      return;\n    }\n\n    var repetido = socios.some(function(s){\n      return s.dni.replace(\/\\.\/g, \"\") !== datos.dni ? false : (!socioEnEdicion || s.fila !== socioEnEdicion.fila);\n    });\n    if (repetido){\n      elAvisoSocio.className = \"aviso-form error\";\n      elAvisoSocio.textContent = \"Ya existe un socio con ese DNI.\";\n      return;\n    }\n    if (socioEnEdicion){ datos.editarFila = socioEnEdicion.fila; datos.dniOriginal = socioEnEdicion.dni; }\n\n    elBtnGuardarSocio.disabled = true;\n    elAvisoSocio.className = \"aviso-form\";\n    elAvisoSocio.textContent = \"Guardando...\";\n    llamarServicio({ accion: \"guardarSocio\", socio: datos })\n      .then(function(res){\n        elBtnGuardarSocio.disabled = false;\n        if (!res.ok){ elAvisoSocio.className = \"aviso-form error\"; elAvisoSocio.textContent = res.mensaje || \"No se pudo guardar.\"; return; }\n        cargarSocios(function(){\n          elModalSocio.hidden = true;\n          if (socioEnEdicion) abrirSocio(socioEnEdicion.fila);\n          else { elCampoSocios.value = datos.dni; buscarSocios(datos.dni); }\n        });\n      })\n      .catch(function(err){\n        elBtnGuardarSocio.disabled = false;\n        elAvisoSocio.className = \"aviso-form error\";\n        elAvisoSocio.textContent = msgErr(err, \"Error de conexi\u00f3n con el servicio.\");\n      });\n  });\n\n  \/\/ ---- Eliminar socio (con confirmaci\u00f3n) ----\n  function abrirConfirmarSocio(s){\n    var n = cantidadPrestamos(s);\n    document.getElementById(\"descConfirmarSocio\").textContent =\n      \"\\u00bfSeguro que quer\\u00e9s eliminar a \" + s.apellido + (s.nombre ? \", \" + s.nombre : \"\") + \" (DNI \" + s.dni + \")? \" +\n      \"Se borra la fila completa\" + (n ? \", incluidos sus \" + n + (n === 1 ? \" pr\u00e9stamo registrado\" : \" pr\u00e9stamos registrados\") : \"\") +\n      \". Esta acci\\u00f3n no se puede deshacer.\";\n    elAvisoConfirmarSocio.className = \"aviso-form\";\n    elAvisoConfirmarSocio.textContent = \"\";\n    elModalConfirmarSocio.hidden = false;\n  }\n  document.getElementById(\"btnCancelarEliminarSocio\").addEventListener(\"click\", function(){ elModalConfirmarSocio.hidden = true; });\n\n  elBtnConfirmarEliminarSocio.addEventListener(\"click\", function(){\n    var s = socioActual;\n    if (!s) return;\n    elBtnConfirmarEliminarSocio.disabled = true;\n    elAvisoConfirmarSocio.className = \"aviso-form\";\n    elAvisoConfirmarSocio.textContent = \"Eliminando...\";\n    llamarServicio({ accion: \"eliminarSocio\", fila: s.fila, dni: s.dni })\n      .then(function(res){\n        elBtnConfirmarEliminarSocio.disabled = false;\n        if (!res.ok){ elAvisoConfirmarSocio.className = \"aviso-form error\"; elAvisoConfirmarSocio.textContent = res.mensaje || \"No se pudo eliminar.\"; return; }\n        elModalConfirmarSocio.hidden = true;\n        cargarSocios(function(){ buscarSocios(ultimaConsultaSocios); });\n      })\n      .catch(function(err){\n        elBtnConfirmarEliminarSocio.disabled = false;\n        elAvisoConfirmarSocio.className = \"aviso-form error\";\n        elAvisoConfirmarSocio.textContent = msgErr(err, \"Error de conexi\u00f3n con el servicio.\");\n      });\n  });\n\n  \/\/ ============================================================\n  \/\/ Informe: Socios con Pr\u00e9stamos \/ Todos los Pr\u00e9stamos\n  \/\/ ============================================================\n  \/\/ ------------------------------------------------------------\n  \/\/ Comunicaci\u00f3n: mensajes entre bibliotecarias (hoja \"Comunicaci\u00f3n\")\n  \/\/ Se muestran los \u00faltimos 5 (del m\u00e1s nuevo hacia abajo) y \"+ Mensajes\" trae los anteriores.\n  \/\/ ------------------------------------------------------------\n  var elTextoMensaje = document.getElementById(\"textoMensaje\");\n  var elContadorMensaje = document.getElementById(\"contadorMensaje\");\n  var elAvisoMensaje = document.getElementById(\"avisoMensaje\");\n  var elBtnEnviarMensaje = document.getElementById(\"btnEnviarMensaje\");\n  var elListaMensajes = document.getElementById(\"listaMensajes\");\n  var elBtnMasMensajes = document.getElementById(\"btnMasMensajes\");\n  var mensajes = [];\n  var hayMasMensajes = false;\n  var cargandoMensajes = false;\n  var MENSAJES_POR_PAGINA = 5;\n\n  function actualizarContadorMensaje(){\n    elContadorMensaje.textContent = elTextoMensaje.value.length + \" \/ 1000\";\n  }\n  elTextoMensaje.addEventListener(\"input\", actualizarContadorMensaje);\n\n  function dibujarMensajes(){\n    if (mensajes.length === 0){\n      elListaMensajes.innerHTML = '<div class=\"mensaje\">Todav\u00eda no hay mensajes.<\/div>';\n    } else {\n      elListaMensajes.innerHTML = mensajes.map(function(m){\n        var propio = normalizar(m.usuario) === normalizar(usuarioActual);\n        return '<article class=\"com-msg com-c' + (Math.abs(parseInt(m.color, 10) || 0) % 10) + '\">' +\n          '<p class=\"com-meta\"><strong>' + escapeHtml(m.usuario) + (propio ? ' (vos)' : '') + '<\/strong> \\u00b7 ' + escapeHtml(m.fecha) + '<\/p>' +\n          '<p class=\"com-texto\">' + escapeHtml(m.texto) + '<\/p><\/article>';\n      }).join(\"\");\n    }\n    elBtnMasMensajes.hidden = !hayMasMensajes;\n  }\n\n  function cargarMensajes(desdeCero){\n    if (cargandoMensajes) return;\n    cargandoMensajes = true;\n    if (desdeCero){\n      mensajes = [];\n      hayMasMensajes = false;\n      elBtnMasMensajes.hidden = true;\n      elListaMensajes.innerHTML = '<div class=\"mensaje\"><div class=\"spinner\"><\/div>Cargando mensajes\u2026<\/div>';\n    }\n    elBtnMasMensajes.disabled = true;\n    var antesDe = mensajes.length !== 0 ? mensajes[mensajes.length - 1].fila : 0;\n    llamarServicio({ accion: \"listarMensajes\", antesDe: antesDe, cantidad: MENSAJES_POR_PAGINA })\n      .then(function(res){\n        cargandoMensajes = false;\n        elBtnMasMensajes.disabled = false;\n        if (!res.ok){\n          if (!res.sesionVencida) elListaMensajes.innerHTML = '<div class=\"mensaje\">' + escapeHtml(res.mensaje || \"No se pudieron leer los mensajes.\") + '<\/div>';\n          return;\n        }\n        mensajes = mensajes.concat(res.mensajes);\n        hayMasMensajes = !!res.hayMas;\n        dibujarMensajes();\n      })\n      .catch(function(err){\n        cargandoMensajes = false;\n        elBtnMasMensajes.disabled = false;\n        elListaMensajes.innerHTML = '<div class=\"mensaje\">' + escapeHtml(msgErr(err, \"Error de conexi\u00f3n.\")) + '<\/div>';\n      });\n  }\n\n  elBtnMasMensajes.addEventListener(\"click\", function(){ cargarMensajes(false); });\n\n  elBtnEnviarMensaje.addEventListener(\"click\", function(){\n    var texto = elTextoMensaje.value.trim();\n    if (!texto){\n      elAvisoMensaje.className = \"aviso-form error\";\n      elAvisoMensaje.textContent = \"Escrib\u00ed un mensaje antes de enviarlo.\";\n      return;\n    }\n    elBtnEnviarMensaje.disabled = true;\n    elAvisoMensaje.className = \"aviso-form\";\n    elAvisoMensaje.textContent = \"Enviando...\";\n    llamarServicio({ accion: \"enviarMensaje\", texto: texto })\n      .then(function(res){\n        elBtnEnviarMensaje.disabled = false;\n        if (!res.ok){\n          elAvisoMensaje.className = \"aviso-form error\";\n          elAvisoMensaje.textContent = res.mensaje || \"No se pudo enviar el mensaje.\";\n          return;\n        }\n        elTextoMensaje.value = \"\";\n        actualizarContadorMensaje();\n        elAvisoMensaje.className = \"aviso-form ok\";\n        elAvisoMensaje.textContent = \"Mensaje enviado.\";\n        cargandoMensajes = false;\n        cargarMensajes(true);\n      })\n      .catch(function(err){\n        elBtnEnviarMensaje.disabled = false;\n        elAvisoMensaje.className = \"aviso-form error\";\n        elAvisoMensaje.textContent = msgErr(err, \"Error de conexi\u00f3n con el servicio.\");\n      });\n  });\n\n  \/\/ ------------------------------------------------------------\n  \/\/ Historial de pr\u00e9stamos por libro o por socio (hoja \"Historial\")\n  \/\/ ------------------------------------------------------------\n  var elResultadoHistorial = document.getElementById(\"resultadoHistorial\");\n  var elEstadoHistorial = document.getElementById(\"estadoHistorial\");\n  var elCampoHistorial = document.getElementById(\"campoHistorial\");\n  var historial = [];\n  var historialCargado = false;\n\n  function cargarHistorial(){\n    elResultadoHistorial.innerHTML = '<div class=\"mensaje\"><div class=\"spinner\"><\/div>Cargando el historial\u2026<\/div>';\n    llamarServicio({ accion: \"listarHistorial\" })\n      .then(function(res){\n        if (!res.ok) throw new Error(res.mensaje || \"No se pudo leer el historial.\");\n        historial = res.filas.map(function(r){\n          var ren = String(r[6] || \"\").trim();\n          return { apellido: String(r[0]).trim(), nombre: String(r[1]).trim(), titulo: String(r[2]).trim(),\n                   inv: String(r[3]).trim(), fecha: String(r[4]).trim(), dev: String(r[5]).trim(),\n                   renov: ren ? ren.split(\/\\s*,\\s*\/) : [], registro: String(r[7] || \"\").trim() };\n        });\n        historialCargado = true;\n        buscarHistorial(elCampoHistorial.value);\n      })\n      .catch(function(err){\n        historialCargado = false;\n        elResultadoHistorial.innerHTML = '<div class=\"mensaje\">' + escapeHtml(msgErr(err, \"Error de conexi\u00f3n.\")) + '<\/div>';\n      });\n  }\n\n  function coincideHistorial(h, palabras){\n    var texto = normalizar([h.apellido, h.nombre, h.titulo].join(\" \"));\n    var tokens = texto.split(\/\\s+\/);\n    var inv = normalizar(h.inv);\n    return palabras.every(function(p){\n      if (\/^\\d+$\/.test(p)) return inv === p ? true : tokens.indexOf(p) !== -1;\n      return texto.indexOf(p) !== -1;\n    });\n  }\n\n  function msPrestamoHistorial(h){\n    var d = datosFecha(h.fecha);\n    return d ? d.ms : 0;\n  }\n\n  function buscarHistorial(consulta){\n    var texto = consulta.trim();\n    if (!texto){\n      elEstadoHistorial.textContent = \"\";\n      elResultadoHistorial.innerHTML = '<div class=\"mensaje\">Hay <strong>' + historial.length +\n        '<\/strong> pr\u00e9stamos en el historial. Escrib\u00ed un libro (t\u00edtulo o N\u00b0 de inventario) o un socio (apellido o nombre).<\/div>';\n      return;\n    }\n    var palabras = normalizar(texto).split(\/\\s+\/).filter(Boolean);\n    var encontrados = historial.filter(function(h){ return coincideHistorial(h, palabras); });\n    if (!encontrados.length){\n      elEstadoHistorial.textContent = \"\";\n      elResultadoHistorial.innerHTML = '<div class=\"mensaje\">No encontramos pr\u00e9stamos para <strong>\\u201c' + escapeHtml(texto) + '\\u201d<\/strong>.<\/div>';\n      return;\n    }\n    encontrados = encontrados.slice().reverse();                       \/\/ lo \u00faltimo cargado primero\n    encontrados.sort(function(a, b){ return msPrestamoHistorial(b) - msPrestamoHistorial(a); });\n    var enCurso = encontrados.filter(function(h){ return !h.dev; }).length;\n    elEstadoHistorial.textContent = encontrados.length + (encontrados.length === 1 ? \" pr\u00e9stamo\" : \" pr\u00e9stamos\") +\n      \" para \\u201c\" + texto + \"\\u201d \\u00b7 \" + enCurso + \" en curso\";\n    elResultadoHistorial.innerHTML = encontrados.map(function(h){\n      var renovado = h.renov.length !== 0;\n      var clase = h.dev ? \" devuelto\" : (renovado ? \" renovado\" : \"\");\n      var nombre = h.apellido + (h.nombre ? \", \" + h.nombre : \"\");\n      return '<article class=\"inf-item' + clase + '\">' +\n        '<p class=\"inf-titulo\"><span class=\"inf-inv\">N\u00b0 ' + escapeHtml(h.inv) + '<\/span> \\u2014 ' + escapeHtml(h.titulo || \"(sin t\u00edtulo)\") + '<\/p>' +\n        '<p class=\"inf-sub\">' + escapeHtml(nombre) + ' \\u00b7 prestado el ' + escapeHtml(h.fecha || \"?\") + ' \\u00b7 ' +\n        (h.dev ? 'devuelto el ' + escapeHtml(h.dev) : '<span class=\"inf-en-curso\">en pr\u00e9stamo<\/span>') +\n        (renovado ? ' \\u00b7 renovado el ' + escapeHtml(h.renov.join(\", \")) : '') + '<\/p>' +\n        (h.registro ? '<p class=\"inf-sub\">Cambios de estado: ' + escapeHtml(h.registro) + '<\/p>' : '') + '<\/article>';\n    }).join(\"\");\n  }\n\n  document.getElementById(\"formHistorial\").addEventListener(\"submit\", function(e){\n    e.preventDefault();\n    if (!historialCargado){ cargarHistorial(); return; }\n    buscarHistorial(elCampoHistorial.value);\n  });\n  elCampoHistorial.addEventListener(\"input\", function(){\n    if (elCampoHistorial.value.trim() === \"\" ? historialCargado : false) buscarHistorial(\"\");\n  });\n\n  var elResultadoInforme = document.getElementById(\"resultadoInforme\");\n  var elEstadoInforme = document.getElementById(\"estadoInforme\");\n  var elBtnInfSocios = document.getElementById(\"btnInfSocios\");\n  var elBtnInfTodos = document.getElementById(\"btnInfTodos\");\n  var DIAS_LIMITE = 5;\n  var informeActivo = null;\n\n  function datosFecha(texto){\n    var m = String(texto || \"\").trim().match(\/^(\\d{1,2})\\\/(\\d{1,2})\\\/(\\d{4})$\/);\n    if (!m) return null;\n    var f = new Date(parseInt(m[3], 10), parseInt(m[2], 10) - 1, parseInt(m[1], 10));\n    var h = new Date();\n    var hoy = new Date(h.getFullYear(), h.getMonth(), h.getDate());\n    return { ms: f.getTime(), dias: Math.round((hoy.getTime() - f.getTime()) \/ 86400000) };\n  }\n\n  \/\/ M\u00e1s de 5 d\u00edas desde la fecha del pr\u00e9stamo.\n  function estaVencido(fecha){\n    var d = datosFecha(fecha);\n    return d ? Math.sign(d.dias - DIAS_LIMITE) === 1 : false;\n  }\n\n  function fechaATexto(f){\n    return (\"0\" + f.getDate()).slice(-2) + \"\/\" + (\"0\" + (f.getMonth() + 1)).slice(-2) + \"\/\" + f.getFullYear();\n  }\n\n  function sumarDias(fechaTexto, dias){\n    var d = datosFecha(fechaTexto);\n    if (!d) return \"\";\n    var f = new Date(d.ms);\n    f.setDate(f.getDate() + dias);\n    return fechaATexto(f);\n  }\n\n  \/\/ Plazo de un pr\u00e9stamo: 5 d\u00edas, m\u00e1s 5 por cada renovaci\u00f3n.\n  function plazoDias(p){ return DIAS_LIMITE * (1 + p.renov.length); }\n  function vencimientoTexto(p){ return sumarDias(p.fecha, plazoDias(p)); }\n  function estaVencidoPrestamo(p){\n    var d = datosFecha(p.fecha);\n    return d ? Math.sign(d.dias - plazoDias(p)) === 1 : false;\n  }\n\n  function tituloDeInventario(inv){\n    var lb = libroPorInventario(inv);\n    return lb ? lb.titulo : \"(no figura en el cat\u00e1logo)\";\n  }\n\n  function mostrarInforme(cual){\n    informeActivo = cual;\n    elBtnInfSocios.classList.toggle(\"activo\", cual === \"socios\");\n    elBtnInfTodos.classList.toggle(\"activo\", cual === \"todos\");\n    elEstadoInforme.textContent = \"\";\n    elResultadoInforme.innerHTML = '<div class=\"mensaje\"><div class=\"spinner\"><\/div>Preparando el informe\u2026<\/div>';\n    \/\/ Siempre se vuelve a leer la planilla para que el informe est\u00e9 actualizado.\n    cargarSocios(function(){\n      if (!socioActual) buscarSocios(ultimaConsultaSocios);\n      if (cual === \"socios\") informeSociosConPrestamos(); else informeTodosLosPrestamos();\n    }, function(mensaje){\n      elResultadoInforme.innerHTML = '<div class=\"mensaje\">' + escapeHtml(mensaje) + '<\/div>';\n    });\n  }\n\n  function informeSociosConPrestamos(){\n    var lista = socios.filter(function(s){ return cantidadPrestamos(s) !== 0; });\n    lista.sort(function(a, b){ return nombreSocio(a).localeCompare(nombreSocio(b), \"es\"); });\n    if (!lista.length){\n      elResultadoInforme.innerHTML = '<div class=\"mensaje\">Ning\u00fan socio tiene libros prestados.<\/div>';\n      return;\n    }\n    var total = 0;\n    var html = lista.map(function(s){\n      var items = s.prestamos.filter(function(p){ return p.inv; });\n      total += items.length;\n      return '<section class=\"inf-socio\"><h3>' + escapeHtml(nombreSocio(s)) + ' <small>DNI ' + escapeHtml(s.dni) + '<\/small><\/h3><ul class=\"inf-lista\">' +\n        items.map(function(p){\n          return '<li><span class=\"inf-inv\">N\u00b0 ' + escapeHtml(p.inv) + '<\/span> \\u2014 ' + escapeHtml(tituloDeInventario(p.inv)) +\n                 (p.fecha ? ' <small>(desde ' + escapeHtml(p.fecha) + ')<\/small>' : '') +\n                 (p.renov.length !== 0 ? ' <small style=\"color:#9a5b00;font-weight:600\">\\u00b7 renovado<\/small>' : '') + '<\/li>';\n        }).join(\"\") + '<\/ul><\/section>';\n    }).join(\"\");\n    elEstadoInforme.textContent = lista.length + (lista.length === 1 ? \" socio\" : \" socios\") + \" con \" + total + (total === 1 ? \" libro prestado\" : \" libros prestados\");\n    elResultadoInforme.innerHTML = html;\n  }\n\n  function informeTodosLosPrestamos(){\n    var filas = [];\n    socios.forEach(function(s){\n      s.prestamos.forEach(function(p){\n        if (p.inv){\n          var d = datosFecha(p.fecha);\n          filas.push({ inv: p.inv, fecha: p.fecha, socio: s, p: p, ms: d ? d.ms : 9e15, dias: d ? d.dias : null });\n        }\n      });\n    });\n    if (!filas.length){\n      elResultadoInforme.innerHTML = '<div class=\"mensaje\">No hay libros prestados.<\/div>';\n      return;\n    }\n    filas.sort(function(a, b){ return a.ms - b.ms; });   \/\/ los m\u00e1s antiguos primero\n    var vencidos = filas.filter(function(f){ return estaVencidoPrestamo(f.p); }).length;\n    elEstadoInforme.textContent = filas.length + (filas.length === 1 ? \" pr\u00e9stamo\" : \" pr\u00e9stamos\") + \" en total \\u00b7 \" +\n      vencidos + \" con plazo vencido (en rojo)\";\n    elResultadoInforme.innerHTML = filas.map(function(f){\n      var venc = estaVencidoPrestamo(f.p);\n      var renovado = f.p.renov.length !== 0;\n      var vence = vencimientoTexto(f.p);\n      var hace = f.dias === null ? \"\" : (f.dias === 0 ? \" \\u00b7 hoy\" : \" \\u00b7 hace \" + f.dias + (f.dias === 1 ? \" d\u00eda\" : \" d\u00edas\"));\n      return '<article class=\"inf-item' + (venc ? ' vencido' : (renovado ? ' renovado' : '')) + '\">' +\n        '<p class=\"inf-titulo\"><span class=\"inf-inv\">N\u00b0 ' + escapeHtml(f.inv) + '<\/span> \\u2014 ' + escapeHtml(tituloDeInventario(f.inv)) + '<\/p>' +\n        '<p class=\"inf-sub\">' + escapeHtml(nombreSocio(f.socio)) + ' \\u00b7 DNI ' + escapeHtml(f.socio.dni) +\n        (f.fecha ? ' \\u00b7 desde ' + escapeHtml(f.fecha) : ' \\u00b7 sin fecha') + hace +\n        (renovado ? ' \\u00b7 renovado el ' + escapeHtml(f.p.renov[f.p.renov.length - 1]) : '') +\n        (vence ? ' \\u00b7 vence ' + vence : '') + '<\/p><\/article>';\n    }).join(\"\");\n  }\n\n  elBtnInfSocios.addEventListener(\"click\", function(){ mostrarInforme(\"socios\"); });\n  elBtnInfTodos.addEventListener(\"click\", function(){ mostrarInforme(\"todos\"); });\n\n  document.getElementById(\"formSocios\").addEventListener(\"submit\", function(e){\n    e.preventDefault();\n    if (!sociosCargados){ cargarSocios(function(){ buscarSocios(elCampoSocios.value); }); return; }\n    buscarSocios(elCampoSocios.value);\n  });\n  elCampoSocios.addEventListener(\"input\", function(){\n    if (elCampoSocios.value.trim() === \"\" ? sociosCargados : false) buscarSocios(\"\");\n  });\n\n})();\n<\/script>\n<\/body>\n<\/html>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cat\u00e1logo en L\u00ednea &#8211; BILIB Acceso bibliotecarios Instituto de Formaci\u00f3n Docente Continua \u00b7 Villa Regina BILIB &#8211; Cat\u00e1logo en L\u00ednea Escrib\u00ed un t\u00edtulo, un autor, un tema o un n\u00famero de inventario y te mostramos los libros del cat\u00e1logo que coinciden. Cat\u00e1logo Pr\u00e9stamos Informe Historial Comunicaci\u00f3n Buscar en el cat\u00e1logo Buscar Modo bibliotecario activo + &#8230; <a title=\"Cat\u00e1logo Digital &#8211; Biblioteca\" class=\"read-more\" href=\"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/biblioteca\/\" aria-label=\"More on Cat\u00e1logo Digital &#8211; Biblioteca\">[+]<\/a><\/p>\n","protected":false},"author":1422,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":[],"acf":[],"_links":{"self":[{"href":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/wp-json\/wp\/v2\/pages\/3113"}],"collection":[{"href":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/wp-json\/wp\/v2\/users\/1422"}],"replies":[{"embeddable":true,"href":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/wp-json\/wp\/v2\/comments?post=3113"}],"version-history":[{"count":53,"href":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/wp-json\/wp\/v2\/pages\/3113\/revisions"}],"predecessor-version":[{"id":3427,"href":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/wp-json\/wp\/v2\/pages\/3113\/revisions\/3427"}],"wp:attachment":[{"href":"https:\/\/ifdvregina-rng.infd.edu.ar\/sitio\/wp-json\/wp\/v2\/media?parent=3113"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}