{"id":2080,"date":"2025-10-08T21:10:28","date_gmt":"2025-10-08T21:10:28","guid":{"rendered":"https:\/\/narino.gov.co\/lamejortaza\/?page_id=2080"},"modified":"2025-10-14T20:39:18","modified_gmt":"2025-10-14T20:39:18","slug":"cafe-de-letras","status":"publish","type":"page","link":"https:\/\/narino.gov.co\/lamejortaza\/cafe-de-letras\/","title":{"rendered":"Caf\u00e9 de Letras"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"2080\" class=\"elementor elementor-2080\" data-elementor-settings=\"{&quot;ha_cmc_init_switcher&quot;:&quot;no&quot;}\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-66eebbe e-con-full e-flex e-con e-parent\" data-id=\"66eebbe\" data-element_type=\"container\" data-settings=\"{&quot;_ha_eqh_enable&quot;:false}\">\n\t\t\t\t<div class=\"elementor-element elementor-element-d0d00d3 elementor-widget elementor-widget-html\" data-id=\"d0d00d3\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"es\">\r\n<head>\r\n  <meta charset=\"utf-8\" \/>\r\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\r\n  <title>Caf\u00e9 de Letras \u2014 Mejor Taza de Nari\u00f1o<\/title>\r\n  <script src=\"https:\/\/cdn.tailwindcss.com\"><\/script>\r\n  <style>\r\n    @keyframes pop { 0%{transform:scale(.95)} 100%{transform:scale(1)} }\r\n    @keyframes wiggle { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-3px)} 75%{transform:translateX(3px)} }\r\n    .pop { animation: pop .15s ease-out; }\r\n    .wiggle { animation: wiggle .18s ease-in-out; }\r\n    .nice-scroll { scrollbar-width: thin; scrollbar-color: #94a3b8 transparent; }\r\n    .coffee-grad { background-image: linear-gradient(to bottom right, #fff7ed, #fef3c7, #f5f5f4); }\r\n    \r\n    \/* Estilos personalizados para el tablero rectangular 20x15 *\/\r\n    #boardWrap {\r\n      max-width: 95vw;\r\n      width: 100%;\r\n      margin: 0 auto;\r\n    }\r\n    \r\n    #board {\r\n      max-width: 95vw;\r\n      max-height: 75vh;\r\n      width: 100%;\r\n      margin: 0 auto;\r\n    }\r\n    \r\n    @media (min-width: 640px) {\r\n      #boardWrap {\r\n        max-width: 90vw;\r\n      }\r\n      #board {\r\n        max-width: min(90vw, 800px);\r\n        max-height: min(75vh, 600px);\r\n      }\r\n    }\r\n    \r\n    @media (min-width: 1024px) {\r\n      #board {\r\n        max-width: min(75vw, 100%);\r\n        max-height: min(80vh, 675px);\r\n      }\r\n    }\r\n  <\/style>\r\n<\/head>\r\n<body class=\"min-h-screen coffee-grad text-slate-800\">\r\n  <!-- Encabezado -->\r\n  <header class=\"max-w-7xl mx-auto px-4 pt-6 pb-3\">\r\n    <div class=\"flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between\">\r\n      <div>\r\n        <h1 class=\"text-2xl sm:text-3xl font-black tracking-tight\">\r\n          Caf\u00e9 de Letras <span class=\"text-amber-700\">\"Mejor Taza de Nari\u00f1o\"<\/span> \u2615\ufe0f\r\n        <\/h1>\r\n        <p class=\"text-sm sm:text-base text-slate-600 max-w-2xl\">\r\n          Encuentra t\u00e9rminos de caf\u00e9: proceso, preparaci\u00f3n y presentaciones. \u00a1Compite contra el reloj!\r\n        <\/p>\r\n      <\/div>\r\n      <div class=\"flex flex-wrap gap-2\">\r\n        <button id=\"btnAyuda\" class=\"px-3 py-1.5 text-sm rounded-2xl bg-amber-500 text-white font-semibold shadow hover:bg-amber-600 active:scale-95\">Ayuda<\/button>\r\n        <button id=\"btnNuevo\" class=\"px-3 py-1.5 text-sm rounded-2xl bg-amber-700 text-white font-semibold shadow hover:bg-amber-800 active:scale-95\">Nueva sopa<\/button>\r\n        <button id=\"btnDeshacer\" class=\"px-3 py-1.5 text-sm rounded-2xl bg-stone-900 text-white font-semibold shadow hover:bg-stone-800 active:scale-95\">Deshacer<\/button>\r\n      <\/div>\r\n    <\/div>\r\n  <\/header>\r\n\r\n  <!-- Contenido principal con nueva distribuci\u00f3n -->\r\n  <main class=\"max-w-8xl mx-auto px-4 pb-12\">\r\n    <div class=\"grid lg:grid-cols-[1fr_280px] gap-6 lg:gap-8 items-start\">\r\n      <!-- Panel juego (\u00e1rea principal m\u00e1s amplia) -->\r\n      <section class=\"w-full\">\r\n        <!-- Marcadores + temporizador -->\r\n        <div class=\"grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4\">\r\n          <div class=\"rounded-2xl bg-white shadow p-3 text-center\">\r\n            <div class=\"text-xs uppercase tracking-wide text-slate-500\">Aciertos<\/div>\r\n            <div id=\"aciertos\" class=\"text-2xl font-black text-emerald-600\">0<\/div>\r\n          <\/div>\r\n          <div class=\"rounded-2xl bg-white shadow p-3 text-center\">\r\n            <div class=\"text-xs uppercase tracking-wide text-slate-500\">Fallos<\/div>\r\n            <div id=\"fallos\" class=\"text-2xl font-black text-rose-600\">0<\/div>\r\n          <\/div>\r\n          <div class=\"rounded-2xl bg-white shadow p-3\">\r\n            <div class=\"text-xs uppercase tracking-wide text-slate-500 text-center mb-2\">Tiempo<\/div>\r\n            <div class=\"flex items-center justify-center gap-2 flex-wrap\">\r\n              <select id=\"duracion\" class=\"text-xs px-2 py-1.5 bg-slate-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-400 font-semibold\">\r\n                <option value=\"120\">2:00<\/option>\r\n                <option value=\"180\" selected>3:00<\/option>\r\n                <option value=\"300\">5:00<\/option>\r\n              <\/select>\r\n              <div id=\"timer\" class=\"text-xl font-black text-amber-700 min-w-[60px] text-center\">03:00<\/div>\r\n              <button id=\"btnIniciar\" class=\"px-3 py-1.5 rounded-lg bg-emerald-600 text-white text-xs font-semibold hover:bg-emerald-700 active:scale-95 transition-transform\">Iniciar<\/button>\r\n              <button id=\"btnReiniciar\" class=\"px-3 py-1.5 rounded-lg bg-slate-200 text-slate-700 text-xs font-semibold hover:bg-slate-300 active:scale-95 transition-transform\">Reiniciar<\/button>\r\n            <\/div>\r\n          <\/div>\r\n          <div class=\"rounded-2xl bg-white shadow p-3 text-center\">\r\n            <div class=\"text-xs uppercase tracking-wide text-slate-500\">Pendientes<\/div>\r\n            <div id=\"pendientes\" class=\"text-2xl font-black text-indigo-600\">0<\/div>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <!-- Tablero con l\u00edmite de 90% y rectangular -->\r\n        <div id=\"boardWrap\" class=\"rounded-3xl bg-white shadow p-2 sm:p-4\">\r\n          <div id=\"board\" class=\"grid gap-0.5 sm:gap-1 select-none touch-none w-full\"><\/div>\r\n        <\/div>\r\n      <\/section>\r\n\r\n      <!-- Panel lateral m\u00e1s angosto (280px en desktop) -->\r\n      <aside class=\"space-y-4 lg:sticky lg:top-4\">\r\n        <div class=\"rounded-2xl bg-white shadow p-4\">\r\n          <div class=\"flex items-center justify-between mb-3\">\r\n            <h2 class=\"font-bold text-base\">Palabras<\/h2>\r\n            <button id=\"btnToggleLista\" class=\"text-xs px-2 py-1 rounded-md bg-slate-100 hover:bg-slate-200\">\r\n              Ocultar\r\n            <\/button>\r\n          <\/div>\r\n          <ul id=\"listaPalabras\" class=\"nice-scroll max-h-[450px] overflow-auto pr-1 text-sm leading-6\"><\/ul>\r\n        <\/div>\r\n        \r\n        <div class=\"rounded-2xl bg-white shadow p-4\">\r\n          <h2 class=\"font-bold text-base mb-3\">C\u00f3mo jugar<\/h2>\r\n          <ol class=\"list-decimal pl-5 space-y-2 text-xs text-slate-700\">\r\n            <li><strong>Arrastra<\/strong> en l\u00ednea recta (\u2194 \u2195 \u2197 \u2198)<\/li>\r\n            <li>Se valida <span class=\"text-emerald-600 font-semibold\">autom\u00e1ticamente<\/span><\/li>\r\n            <li>Fallos muestran <span class=\"text-rose-600 font-semibold\">error<\/span><\/li>\r\n            <li>El <strong>tiempo<\/strong> es limitado<\/li>\r\n          <\/ol>\r\n        <\/div>\r\n      <\/aside>\r\n    <\/div>\r\n  <\/main>\r\n\r\n  <footer class=\"max-w-6xl mx-auto px-4 pb-8 text-xs text-slate-500 text-center\">\r\n    Tema: Caf\u00e9 de Nari\u00f1o \u2022 Tablero 20\u00d715 \u2022 Optimizado para mejor visualizaci\u00f3n\r\n  <\/footer>\r\n\r\n  <script>\r\n    \/\/ ======== Palabras tema \"Mejor Taza de Nari\u00f1o\" ========\r\n    const palabrasOriginales = [\r\n      \"Cereza\", \"Despulpado\", \"Fermentaci\u00f3n\", \"Lavado\", \"Secado\",\r\n      \"Pergamino\", \"Trilla\", \"Tosti\u00f3n\", \"Molienda\", \"Cataci\u00f3n\",\r\n      \"Altura\", \"Varietal\", \"Ar\u00e1bica\", \"Perfil\", \"Acidez\", \"Cuerpo\",\r\n      \"Fragancia\", \"Aroma\", \"Notas\", \"Origen\", \"Calidad\",\r\n      \"Extracci\u00f3n\", \"Espresso\", \"V60\", \"Chemex\", \"Aeropress\", \"PrensaFrancesa\",\r\n      \"Latte\", \"Capuchino\", \"Americano\", \"Macchiato\",\r\n      \"Mejor Taza de Nari\u00f1o\"\r\n    ].map(s => s.trim());\r\n\r\n    \/\/ ======== Normalizador ========\r\n    const norm = (s) => s\r\n      .toUpperCase()\r\n      .normalize('NFD')\r\n      .replace(\/\\p{Diacritic}\/gu, '')\r\n      .replace(\/\\s+\/g, '');\r\n\r\n    const palabras = palabrasOriginales.map(p => ({ display: p, norm: norm(p) }));\r\n\r\n    \/\/ ======== Par\u00e1metros del tablero 20x15 ========\r\n    const WIDTH = 30;   \/\/ Columnas\r\n    const HEIGHT = 15;  \/\/ Filas\r\n    const DIRS = [\r\n      {dx: 1, dy: 0},{dx: -1, dy: 0},{dx: 0, dy: 1},{dx: 0, dy: -1},\r\n      {dx: 1, dy: 1},{dx: -1, dy: -1},{dx: 1, dy: -1},{dx: -1, dy: 1},\r\n    ];\r\n\r\n    \/\/ ======== Estado ========\r\n    let grid, locked, foundMap, placedWords, aciertos = 0, fallos = 0;\r\n    let isDown = false, dirSel = null, path = [];\r\n    let juegoBloqueado = false;\r\n\r\n    \/\/ Temporizador\r\n    let timerId = null, restante = 180, iniciado = false;\r\n\r\n    \/\/ ======== DOM ========\r\n    const $board = document.getElementById('board');\r\n    const $aciertos = document.getElementById('aciertos');\r\n    const $fallos = document.getElementById('fallos');\r\n    const $pend = document.getElementById('pendientes');\r\n    const $lista = document.getElementById('listaPalabras');\r\n    const $timer = document.getElementById('timer');\r\n    const $duracion = document.getElementById('duracion');\r\n    const $btnIniciar = document.getElementById('btnIniciar');\r\n    const $btnReiniciar = document.getElementById('btnReiniciar');\r\n    const $btnToggleLista = document.getElementById('btnToggleLista');\r\n\r\n    \/\/ ======== Utilidades ========\r\n    const inBounds = (x,y) => x>=0 && y>=0 && x<WIDTH && y<HEIGHT;\r\n    const randInt = (a,b) => Math.floor(Math.random()*(b-a+1))+a;\r\n    const randChoice = (arr) => arr[Math.floor(Math.random()*arr.length)];\r\n    const letterPool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\r\n    const reverse = s => s.split('').reverse().join('');\r\n    const fmt = (s) => `${String(Math.floor(s\/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;\r\n\r\n    \/\/ ======== Temporizador ========\r\n    function setDuracion(sec){ restante = sec; $timer.textContent = fmt(restante); }\r\n    function iniciarTimer(){\r\n      if (timerId) return;\r\n      if (restante <= 0) setDuracion(parseInt($duracion.value,10) || 180);\r\n      iniciado = true;\r\n      timerId = setInterval(()=>{\r\n        restante--;\r\n        $timer.textContent = fmt(restante);\r\n        if(restante === 20) toast('\u00a120 segundos restantes!','bg-rose-600');\r\n        if(restante <= 10) $timer.classList.add('text-rose-700');\r\n        if(restante <= 0){ clearInterval(timerId); timerId = null; tiempoAgotado(); }\r\n      }, 1000);\r\n      toast('\u00a1Tiempo en marcha!','bg-emerald-600');\r\n    }\r\n    function pararTimer(){ if(timerId){ clearInterval(timerId); timerId = null; } }\r\n    function reiniciarTimer(){\r\n      pararTimer(); setDuracion(parseInt($duracion.value,10) || 180);\r\n      iniciado = false; $timer.classList.remove('text-rose-700');\r\n      toast('Temporizador reiniciado','bg-slate-700');\r\n    }\r\n    function tiempoAgotado(){\r\n      juegoBloqueado = true; $timer.textContent = '00:00'; shakeBoard();\r\n      toast('\u23f1\ufe0f \u00a1Tiempo agotado! El tablero se ha bloqueado.','bg-rose-700');\r\n      for(let y=0;y<HEIGHT;y++){ \r\n        for(let x=0;x<WIDTH;x++){ \r\n          const btn = getBtn(x,y); \r\n          if(btn) btn.disabled = true; \r\n        } \r\n      }\r\n    }\r\n    function shakeBoard(){ \r\n      const wrap = document.getElementById('boardWrap'); \r\n      wrap.classList.add('wiggle'); \r\n      setTimeout(()=>wrap.classList.remove('wiggle'), 250); \r\n    }\r\n\r\n    \/\/ ======== Juego ========\r\n    function nuevaSopa(){\r\n      grid = Array.from({length: HEIGHT}, () => Array.from({length: WIDTH}, () => ''));\r\n      locked = Array.from({length: HEIGHT}, () => Array.from({length: WIDTH}, () => false));\r\n      foundMap = new Map(palabras.map(p => [p.norm, false]));\r\n      placedWords = new Map();\r\n      aciertos = 0; fallos = 0; dirSel = null; path = []; isDown = false; juegoBloqueado = false;\r\n\r\n      const orden = [...palabras].sort((a,b)=>b.norm.length-a.norm.length);\r\n      for(const w of orden){ colocarPalabra(w.norm); }\r\n      \r\n      for(let y=0;y<HEIGHT;y++){ \r\n        for(let x=0;x<WIDTH;x++){ \r\n          if(!grid[y][x]) grid[y][x] = randChoice(letterPool); \r\n        } \r\n      }\r\n\r\n      renderBoard();\r\n      renderLista();\r\n      actualizarMarcadores();\r\n      reiniciarTimer();\r\n    }\r\n\r\n    function colocarPalabra(word){\r\n      const L = word.length;\r\n      \r\n      \/\/ Intentar colocaci\u00f3n aleatoria\r\n      for(let attempt=0; attempt<800; attempt++){\r\n        const dir = randChoice(DIRS);\r\n        \r\n        \/\/ Calcular rangos v\u00e1lidos seg\u00fan la direcci\u00f3n\r\n        let minX, maxX, minY, maxY;\r\n        \r\n        if(dir.dx === 1){\r\n          minX = 0;\r\n          maxX = WIDTH - L;\r\n        } else if(dir.dx === -1){\r\n          minX = L - 1;\r\n          maxX = WIDTH - 1;\r\n        } else {\r\n          minX = 0;\r\n          maxX = WIDTH - 1;\r\n        }\r\n        \r\n        if(dir.dy === 1){\r\n          minY = 0;\r\n          maxY = HEIGHT - L;\r\n        } else if(dir.dy === -1){\r\n          minY = L - 1;\r\n          maxY = HEIGHT - 1;\r\n        } else {\r\n          minY = 0;\r\n          maxY = HEIGHT - 1;\r\n        }\r\n        \r\n        \/\/ Validar que los rangos sean v\u00e1lidos\r\n        if(minX > maxX || minY > maxY) continue;\r\n\r\n        const x0 = randInt(minX, maxX);\r\n        const y0 = randInt(minY, maxY);\r\n\r\n        \/\/ Verificar si todas las posiciones son v\u00e1lidas\r\n        let ok = true;\r\n        for(let i=0;i<L;i++){\r\n          const x = x0 + dir.dx*i;\r\n          const y = y0 + dir.dy*i;\r\n          \r\n          \/\/ Validar l\u00edmites\r\n          if(!inBounds(x, y)){\r\n            ok = false;\r\n            break;\r\n          }\r\n          \r\n          const c = grid[y][x]; \r\n          if(c && c !== word[i]){ \r\n            ok = false; \r\n            break; \r\n          }\r\n        }\r\n        if(!ok) continue;\r\n\r\n        \/\/ Colocar la palabra\r\n        const coords = [];\r\n        for(let i=0;i<L;i++){\r\n          const x = x0 + dir.dx*i;\r\n          const y = y0 + dir.dy*i;\r\n          grid[y][x] = word[i]; \r\n          coords.push({x,y});\r\n        }\r\n        placedWords.set(word, coords);\r\n        return true;\r\n      }\r\n      \r\n      \/\/ Intentar colocaci\u00f3n sistem\u00e1tica si fall\u00f3 la aleatoria\r\n      for(const dir of DIRS){\r\n        for(let y=0;y<HEIGHT;y++){\r\n          for(let x=0;x<WIDTH;x++){\r\n            const coords = colocaSec(dir, x, y, word);\r\n            if(coords){ \r\n              placedWords.set(word, coords); \r\n              return true; \r\n            }\r\n          }\r\n        }\r\n      }\r\n      \r\n      console.warn(`No se pudo colocar la palabra: ${word}`);\r\n      return false;\r\n    }\r\n\r\n    function colocaSec(dir, x0, y0, word){\r\n      const L = word.length; \r\n      let x = x0, y = y0;\r\n      for(let i=0;i<L;i++){\r\n        if(!inBounds(x,y)) return null;\r\n        const c = grid[y][x]; \r\n        if(c && c !== word[i]) return null;\r\n        x += dir.dx; \r\n        y += dir.dy;\r\n      }\r\n      x = x0; y = y0; \r\n      const coords = [];\r\n      for(let i=0;i<L;i++){ \r\n        grid[y][x] = word[i]; \r\n        coords.push({x,y}); \r\n        x += dir.dx; \r\n        y += dir.dy; \r\n      }\r\n      return coords;\r\n    }\r\n\r\n    function renderBoard(){\r\n      $board.innerHTML = '';\r\n      $board.style.gridTemplateColumns = `repeat(${WIDTH}, minmax(0,1fr))`;\r\n      \r\n      for(let y=0;y<HEIGHT;y++){\r\n        for(let x=0;x<WIDTH;x++){\r\n          const cell = document.createElement('button');\r\n          cell.type = 'button'; \r\n          cell.dataset.x = x; \r\n          cell.dataset.y = y;\r\n          cell.className = 'aspect-square rounded-md sm:rounded-lg flex items-center justify-center font-extrabold text-stone-800 bg-stone-100 hover:bg-stone-200 focus:outline-none focus:ring-2 focus:ring-amber-400 text-xs sm:text-sm md:text-base transition-colors';\r\n          cell.textContent = grid[y][x];\r\n          cell.setAttribute('aria-label', `Fila ${y+1}, Columna ${x+1}, letra ${grid[y][x]}`);\r\n          $board.appendChild(cell);\r\n        }\r\n      }\r\n    }\r\n\r\n    \/\/ ======== Lista visible ========\r\n    function renderLista(){\r\n      $lista.innerHTML = '';\r\n      for(const p of palabras){\r\n        const li = document.createElement('li');\r\n        li.dataset.norm = p.norm;\r\n        li.className = 'flex items-center gap-2 py-1';\r\n        li.innerHTML = `\r\n          <span class=\"w-4 h-4 inline-flex items-center justify-center rounded-full bg-slate-200 text-[10px] text-slate-700 flex-shrink-0\">\u2022<\/span>\r\n          <span class=\"text-xs sm:text-sm\">${p.display}<\/span>\r\n        `;\r\n        $lista.appendChild(li);\r\n      }\r\n    }\r\n    \r\n    function marcarLista(normWord){\r\n      const li = $lista.querySelector(`[data-norm=\"${normWord}\"]`);\r\n      if(!li) return;\r\n      const dot = li.querySelector('span');\r\n      dot.className = 'w-4 h-4 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-[10px] flex-shrink-0';\r\n      dot.textContent = '\u2713';\r\n      li.classList.add('line-through','text-slate-400','bg-emerald-50','ring','ring-emerald-200','rounded-lg','px-2');\r\n      li.scrollIntoView({block:'nearest', behavior:'smooth'});\r\n    }\r\n\r\n    function actualizarMarcadores(){\r\n      $aciertos.textContent = aciertos;\r\n      $fallos.textContent = fallos;\r\n      const pendientes = [...foundMap.values()].filter(v=>!v).length;\r\n      $pend.textContent = pendientes;\r\n    }\r\n\r\n    \/\/ ======== Selecci\u00f3n ========\r\n    function cellFromEvent(e){ \r\n      const tgt = e.target.closest('button'); \r\n      if(!tgt || !$board.contains(tgt)) return null; \r\n      return tgt; \r\n    }\r\n    \r\n    function startSel(btn){\r\n      if(!btn || juegoBloqueado) return;\r\n      if(btn.disabled) return;\r\n      if(!iniciado) iniciarTimer();\r\n      isDown = true; \r\n      path = []; \r\n      dirSel = null; \r\n      addToPath(btn);\r\n    }\r\n    \r\n    function addToPath(btn){\r\n      if(juegoBloqueado) return;\r\n      const x = +btn.dataset.x, y = +btn.dataset.y;\r\n      if(locked[y][x]) return;\r\n      \r\n      if(path.length===0){ \r\n        path.push({x,y,btn}); \r\n        btn.classList.add('bg-amber-300'); \r\n        return; \r\n      }\r\n      \r\n      const first = path[0], last = path[path.length-1];\r\n      if(last.x===x && last.y===y) return;\r\n      \r\n      if(!dirSel){\r\n        const dx = Math.sign(x - first.x), dy = Math.sign(y - first.y);\r\n        if(!(dx===0 || dy===0 || Math.abs(dx)===Math.abs(dy))) return;\r\n        if(dx===0 && dy===0) return;\r\n        dirSel = {dx, dy};\r\n      }\r\n      \r\n      let cx = last.x + dirSel.dx, cy = last.y + dirSel.dy;\r\n      while(!(cx===x && cy===y)){\r\n        if(!inBounds(cx,cy) || locked[cy][cx]) return;\r\n        const interBtn = getBtn(cx,cy); \r\n        if(!interBtn) return;\r\n        path.push({x:cx,y:cy,btn:interBtn}); \r\n        interBtn.classList.add('bg-amber-300');\r\n        cx += dirSel.dx; \r\n        cy += dirSel.dy;\r\n      }\r\n      path.push({x,y,btn}); \r\n      btn.classList.add('bg-amber-300');\r\n    }\r\n    \r\n    function endSel(){ \r\n      if(!isDown) return; \r\n      isDown = false; \r\n      comprobarSeleccion(); \r\n    }\r\n    \r\n    function limpiarSeleccionVisual(){ \r\n      for(const p of path){ \r\n        p.btn.classList.remove('bg-amber-300'); \r\n      } \r\n      path = []; \r\n      dirSel = null; \r\n    }\r\n    \r\n    function getBtn(x,y){ \r\n      return $board.children[y*WIDTH + x]; \r\n    }\r\n    \r\n    function stringFromPath(){ \r\n      return path.map(p => grid[p.y][p.x]).join(''); \r\n    }\r\n\r\n    function bloquearPathOK(){\r\n      for(const p of path){\r\n        p.btn.classList.remove('bg-amber-300');\r\n        p.btn.classList.add('bg-emerald-600','text-white','ring-2','ring-emerald-300','pop','animate-pulse');\r\n        setTimeout(()=>p.btn.classList.remove('animate-pulse'), 500);\r\n        locked[p.y][p.x] = true; \r\n        p.btn.disabled = true;\r\n      }\r\n      path = []; \r\n      dirSel = null;\r\n    }\r\n\r\n    function comprobarSeleccion(){\r\n      const s = norm(stringFromPath());\r\n      const pendientes = palabras.filter(w => !foundMap.get(w.norm));\r\n      let ok = null;\r\n      \r\n      for(const w of pendientes){ \r\n        if(s===w.norm || s===reverse(w.norm)){ \r\n          ok = w; \r\n          break; \r\n        } \r\n      }\r\n      \r\n      if(ok){\r\n        foundMap.set(ok.norm, true);\r\n        aciertos++; \r\n        bloquearPathOK(); \r\n        marcarLista(ok.norm); \r\n        actualizarMarcadores();\r\n        toast(`\u00a1Correcto! \"${ok.display}\"`,'bg-emerald-700');\r\n        if([...foundMap.values()].every(v=>v)){ \r\n          pararTimer(); \r\n          celebrar(); \r\n        }\r\n      }else{\r\n        for(const p of path){ \r\n          p.btn.classList.add('wiggle'); \r\n          setTimeout(()=>p.btn.classList.remove('wiggle'), 220); \r\n        }\r\n        fallos++; \r\n        actualizarMarcadores(); \r\n        toast('No coincide. Intenta de nuevo.','bg-rose-600'); \r\n        limpiarSeleccionVisual();\r\n      }\r\n    }\r\n\r\n    function celebrar(){\r\n      const wrap = document.createElement('div');\r\n      wrap.className = 'fixed inset-0 pointer-events-none overflow-hidden';\r\n      document.body.appendChild(wrap);\r\n      \r\n      for(let i=0;i<120;i++){\r\n        const d = document.createElement('div');\r\n        d.className = 'absolute w-1.5 h-1.5 rounded-full bg-amber-600';\r\n        d.style.left = Math.random()*100 + 'vw'; \r\n        d.style.top = '-5vh';\r\n        const dur = 1200 + Math.random()*800;\r\n        d.animate([\r\n          { transform: `translateY(0)`, opacity:1 }, \r\n          { transform: `translateY(${100+Math.random()*40}vh)`, opacity:0.9 }\r\n        ], { \r\n          duration: dur, \r\n          easing: 'cubic-bezier(.2,.6,.2,1)', \r\n          iterations: 1, \r\n          delay: Math.random()*400 \r\n        });\r\n        setTimeout(()=>d.remove(), dur+500); \r\n        wrap.appendChild(d);\r\n      }\r\n      setTimeout(()=>wrap.remove(), 2500);\r\n      toast('\u00a1Completaste la sopa! \u2615\ufe0f\ud83c\udfc6','bg-amber-700');\r\n    }\r\n\r\n    \/\/ ======== Ayuda \/ Pista ========\r\n    function pista(){\r\n      const pendientes = palabras.filter(w => !foundMap.get(w.norm));\r\n      if(!pendientes.length){ \r\n        toast('\u00a1Ya encontraste todas las palabras! \ud83c\udf89','bg-emerald-600'); \r\n        return; \r\n      }\r\n      \r\n      const w = randChoice(pendientes);\r\n      const coords = placedWords.get(w.norm);\r\n      if(!coords){ \r\n        toast('No hay pista disponible a\u00fan.','bg-slate-700'); \r\n        return; \r\n      }\r\n      \r\n      const celdas = coords.map(({x,y}) => getBtn(x,y));\r\n      celdas.forEach(btn => btn.classList.add('bg-amber-300','ring-2','ring-amber-400','animate-pulse'));\r\n      setTimeout(()=>{ \r\n        celdas.forEach(btn => btn.classList.remove('bg-amber-300','ring-2','ring-amber-400','animate-pulse')); \r\n      }, 1400);\r\n      toast(`Pista: empieza por \"${w.display.toString().charAt(0)}\"`, 'bg-amber-600');\r\n    }\r\n\r\n    function toast(msg, bgClass='bg-slate-900'){\r\n      const t = document.createElement('div');\r\n      t.className = `${bgClass} text-white px-3 py-2 rounded-xl shadow-lg text-sm fixed top-4 right-4 z-50`;\r\n      t.textContent = msg; \r\n      document.body.appendChild(t);\r\n      setTimeout(()=>{ \r\n        t.style.opacity = '0'; \r\n        t.style.transition = 'opacity .35s'; \r\n      }, 1200);\r\n      setTimeout(()=> t.remove(), 1700);\r\n    }\r\n\r\n    \/\/ ======== Eventos ========\r\n    $board.addEventListener('mousedown', (e)=>{ \r\n      const btn = cellFromEvent(e); \r\n      startSel(btn); \r\n    });\r\n    \r\n    $board.addEventListener('mouseover', (e)=>{ \r\n      if(!isDown) return; \r\n      const btn = cellFromEvent(e); \r\n      if(btn) addToPath(btn); \r\n    });\r\n    \r\n    window.addEventListener('mouseup', endSel);\r\n\r\n    $board.addEventListener('touchstart', (e)=>{\r\n      const t = e.touches[0]; \r\n      const el = document.elementFromPoint(t.clientX, t.clientY);\r\n      const btn = el && el.closest('button'); \r\n      startSel(btn);\r\n    }, {passive:true});\r\n    \r\n    $board.addEventListener('touchmove', (e)=>{\r\n      if(!isDown) return; \r\n      const t = e.touches[0]; \r\n      const el = document.elementFromPoint(t.clientX, t.clientY);\r\n      const btn = el && el.closest('button'); \r\n      if(btn) addToPath(btn);\r\n    }, {passive:true});\r\n    \r\n    window.addEventListener('touchend', endSel, {passive:true});\r\n\r\n    document.getElementById('btnNuevo').addEventListener('click', ()=>{ \r\n      nuevaSopa(); \r\n      toast('Nueva sopa generada','bg-amber-700'); \r\n    });\r\n    \r\n    document.getElementById('btnDeshacer').addEventListener('click', limpiarSeleccionVisual);\r\n    document.getElementById('btnAyuda').addEventListener('click', pista);\r\n\r\n    $duracion.addEventListener('change', ()=>{ \r\n      if(!iniciado){ \r\n        setDuracion(parseInt($duracion.value,10)); \r\n      } \r\n    });\r\n    \r\n    $btnIniciar.addEventListener('click', iniciarTimer);\r\n    $btnReiniciar.addEventListener('click', reiniciarTimer);\r\n\r\n    \/\/ Ocultar\/mostrar lista\r\n    $btnToggleLista.addEventListener('click', ()=>{\r\n      if($lista.classList.contains('hidden')){\r\n        $lista.classList.remove('hidden'); \r\n        $btnToggleLista.textContent = 'Ocultar';\r\n      }else{\r\n        $lista.classList.add('hidden'); \r\n        $btnToggleLista.textContent = 'Mostrar';\r\n      }\r\n    });\r\n\r\n    \/\/ ======== INICIALIZAR ========\r\n    console.log('Inicializando sopa de letras 20x15...');\r\n    nuevaSopa();\r\n    console.log('\u2705 Sopa de letras cargada correctamente');\r\n  <\/script>\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Caf\u00e9 de Letras \u2014 Mejor Taza de Nari\u00f1o Caf\u00e9 de Letras \u00abMejor Taza de Nari\u00f1o\u00bb \u2615\ufe0f Encuentra t\u00e9rminos de caf\u00e9: [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"site-sidebar-layout":"no-sidebar","site-content-layout":"","ast-site-content-layout":"full-width-container","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"disabled","ast-breadcrumbs-content":"","ast-featured-img":"disabled","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"class_list":["post-2080","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/narino.gov.co\/lamejortaza\/wp-json\/wp\/v2\/pages\/2080","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/narino.gov.co\/lamejortaza\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/narino.gov.co\/lamejortaza\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/narino.gov.co\/lamejortaza\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/narino.gov.co\/lamejortaza\/wp-json\/wp\/v2\/comments?post=2080"}],"version-history":[{"count":133,"href":"https:\/\/narino.gov.co\/lamejortaza\/wp-json\/wp\/v2\/pages\/2080\/revisions"}],"predecessor-version":[{"id":2506,"href":"https:\/\/narino.gov.co\/lamejortaza\/wp-json\/wp\/v2\/pages\/2080\/revisions\/2506"}],"wp:attachment":[{"href":"https:\/\/narino.gov.co\/lamejortaza\/wp-json\/wp\/v2\/media?parent=2080"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}