gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

feat(web): la franja es la misma de gioser.net y tawasuyu.net, y el enlace compartido trae tarjeta

- franja.js: copia de la de gioser-web (el símbolo que sube, las pestañas que se
  vuelven menú, deslizar entre secciones); el index sólo dice qué sección mostrar
- elegir una sección pide /visto?t=<sección>, sin cookies: lo cuenta el registro
  propio de Caddy
- og:image 1200x630 y twitter:card grande; la descripción decía 941 recetas y
  926 selladas, hoy son 954 y todas selladas

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

7e792166505826cfe492f1b0dc47580d91427f74 · ruwaq sergio · (8 p'unchaw ñawpaq) · tayta 04e686364 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 8

web/takana-os.net/franja.js

3 qillqakuna · +261 −110

diff --git a/web/takana-os.net/franja.js b/web/takana-os.net/franja.js
new file mode 100644
index 00000000..5a64d1d2
--- /dev/null
+++ b/web/takana-os.net/franja.js
@@ -0,0 +1,223 @@
+/* franja.js — la franja de los sitios de gioser: gioser.net, tawasuyu.net y takana-os.net.
+ *
+ * UNA sola fuente. Vive en gioser-web (`franja.js`) y se copia TAL CUAL a tawasuyu
+ * (`web/tawasuyu-web/franja.js`) y a takana (`web/takana-os.net/franja.js`): esos dos sitios no le
+ * piden nada a terceros, así que no la pueden pedir a gioser.net. `scripts/franja-copias.sh`
+ * compara las tres y dice cuál se separó. Antes eran tres copias escritas a mano, y un mismo
+ * defecto —el enlace al inicio que le robaba el clic a la primera pestaña— hubo que arreglarlo
+ * tres veces, una de ellas de otra manera (14-oct).
+ *
+ * Lo que hace, igual en los tres:
+ *   · el símbolo sube desde la portada con el primer scroll y se planta en la franja;
+ *   · la franja se pinta clara sobre la portada y con el color del texto sobre el cuerpo;
+ *   · lo que se pasa por debajo de la franja (la tarjeta de la portada) se apaga antes de tocarla;
+ *   · cuando las pestañas llegan arriba se encogen y se acomodan en la franja: el menú son ellas;
+ *   · se pasa de una pestaña a otra deslizando, como un PageView de Flutter (el panel sigue al
+ *     dedo, entra el vecino desde ese lado, rebota en el borde; lo que se desplaza de costado
+ *     manda hasta su borde; el gesto se reclama para que el navegador no lo tome como «volver»);
+ *   · elegir una pestaña a mano manda una señal mínima a `/visto?t=<pestaña>` (sin cookies ni
+ *     identificadores: el `#` de la dirección nunca llega al servidor, y sin esto no hay forma de
+ *     saber qué se lee). Caddy contesta 204 y lo anota en su registro.
+ *
+ * Lo que es de cada sitio entra por las opciones: qué elementos, cómo se elige una pestaña (en
+ * tawasuyu hay que traer el catálogo primero), qué panel está a la vista.
+ */
+(function () {
+  'use strict';
+
+  var suave = function (t) { return t * t * (3 - 2 * t); };
+  var RAMPA = 90;   // px de scroll en que las pestañas se vuelven menú
+
+  function Franja(o) {
+    var quieto = matchMedia('(prefers-reduced-motion: reduce)').matches;
+    var sc = o.scroller || null;                 // el que se desplaza; null = la ventana
+    var pos = function () { return sc ? sc.scrollTop : scrollY; };
+    var ir = function (top, suavecito) {
+      var op = { top: top, behavior: (suavecito && !quieto) ? 'smooth' : 'auto' };
+      if (sc) sc.scrollTo(op); else scrollTo(op);
+    };
+    var franja = o.franja, marca = o.marca, grande = o.grande, glifos = o.glifos;
+    var barra = o.barra, tabs = o.tabs, apagar = o.apagar || [];
+    var esc = o.escala || [0.55, 0.92], margen = o.margen || 104;
+    var geo = null, menu = null, p = 0;
+    document.documentElement.classList.add('js');
+
+    function medir() {
+      var s = pos(), alto = franja.offsetHeight;
+      if (marca && grande) {
+        marca.style.transform = 'none';
+        var h = grande.getBoundingClientRect(), b = marca.getBoundingClientRect();
+        geo = { hx: h.left, hy: h.top + s, hw: h.width, bx: b.left, by: b.top, bw: b.width };
+      }
+      barra.style.setProperty('--p', 0);
+      tabs.forEach(function (t) { t.style.transform = 'none'; });
+      var top = alto - barra.offsetHeight;          // pegada, asoma sólo su franja de abajo
+      barra.style.setProperty(o.varTop, top + 'px');
+      // un sticky reporta su posición PEGADA: el lugar natural de la barra lo dice el sitio
+      var docTop = s + o.inicioBarra();
+      var br = barra.getBoundingClientRect();
+      var rot = tabs.map(function (t) {
+        var l = t.querySelector(o.rotulo).getBoundingClientRect(), b = t.getBoundingClientRect();
+        return { cx: l.left + l.width / 2, w: l.width, cy: l.top + l.height / 2 - br.top,
+                 ox: l.left + l.width / 2 - b.left, oy: l.top + l.height / 2 - b.top };
+      });
+      // dónde caben en la franja: entre el símbolo (con su palabra) y los glifos
+      var izq = (marca ? marca.getBoundingClientRect().right : 0) + (innerWidth < 720 ? 14 : margen);
+      var der = (glifos ? glifos.getBoundingClientRect().left : innerWidth) - 14;
+      var gap = innerWidth < 720 ? 12 : 22;
+      var suma = rot.reduce(function (a, l) { return a + l.w; }, 0);
+      var k = Math.max(esc[0], Math.min(esc[1], (der - izq - gap * (rot.length - 1)) / suma));
+      var total = suma * k + gap * (rot.length - 1);
+      var x0 = innerWidth / 2 - total / 2;
+      if (x0 < izq) x0 = izq;
+      if (x0 + total > der) x0 = Math.max(izq, der - total);
+      var x = x0;
+      rot.forEach(function (l) { l.dx = x + l.w * k / 2 - l.cx; x += l.w * k + gap; });
+      menu = { alto: alto, top: top, pegado: docTop - top, rot: rot, k: k };
+      tabs.forEach(function (t, i) { t.style.transformOrigin = rot[i].ox + 'px ' + rot[i].oy + 'px'; });
+    }
+
+    function volar(s) {
+      if (!geo) return;
+      // el símbolo sube con la portada hasta la altura de la franja, y en ese trayecto se
+      // corre a su lugar y se achica
+      var t = Math.min(1, Math.max(0, s / Math.max(1, geo.hy - geo.by))), e = suave(t);
+      var x = geo.hx + (geo.bx - geo.hx) * e, y = Math.max(geo.by, geo.hy - s);
+      var k = geo.hw / geo.bw + (1 - geo.hw / geo.bw) * e;
+      marca.style.transform = 'translate(' + (x - geo.bx).toFixed(1) + 'px, ' + (y - geo.by).toFixed(1) +
+        'px) scale(' + k.toFixed(3) + ')';
+      franja.classList.toggle('llego', t > 0.98);
+    }
+
+    function encoger(s) {
+      if (!menu) return;
+      var t = Math.min(1, Math.max(0, (s - (menu.pegado - RAMPA)) / RAMPA));
+      p = suave(t);
+      barra.style.setProperty('--p', p.toFixed(3));
+      var falta = Math.max(0, menu.pegado - s);
+      tabs.forEach(function (tab, i) {
+        var l = menu.rot[i], dy = menu.alto / 2 - (menu.top + l.cy + falta), k = 1 - (1 - menu.k) * p;
+        tab.style.transform = p === 0 ? 'none'
+          : 'translate(' + (l.dx * p).toFixed(1) + 'px, ' + (dy * p).toFixed(1) + 'px) scale(' + k.toFixed(3) + ')';
+      });
+      franja.classList.toggle('con-menu', p > 0.5);
+    }
+
+    var tick = false;
+    function pintar() {
+      tick = false;
+      var s = pos(), alto = franja.offsetHeight;
+      volar(s); encoger(s);
+      apagar.forEach(function (el) {
+        var tp = el.getBoundingClientRect().top;
+        el.style.opacity = Math.min(1, Math.max(0, (tp - alto) / (alto * 1.5))).toFixed(3);
+      });
+      if (o.cuerpo) franja.classList.toggle('sobre-portada', o.cuerpo.getBoundingClientRect().top > alto);
+    }
+    (sc || window).addEventListener('scroll', function () {
+      if (!tick) { tick = true; requestAnimationFrame(pintar); }
+    }, { passive: true });
+    addEventListener('resize', function () { medir(); pintar(); }, { passive: true });
+
+    function irALista() { if (menu) ir(Math.round(menu.pegado), true); }
+
+    // ── elegir: lo de cada sitio, más la señal de lectura ──
+    var indice = function () {
+      return tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; });
+    };
+    function senal(id) {
+      if (!o.senal || !navigator.sendBeacon) return;
+      try { navigator.sendBeacon('/visto?t=' + encodeURIComponent(id)); } catch (e) { /* sin señal */ }
+    }
+    function elegir(id, como) {   // como: {foco, comoMenu, desde: 'der'|'izq'}
+      como = como || {};
+      return Promise.resolve(o.elegir(id)).then(function () {
+        senal(id);
+        if (como.foco) { var t = tabs.find(function (x) { return o.id(x) === id; }); if (t) t.focus(); }
+        if (como.desde) {
+          var nuevo = o.panel();
+          if (nuevo) {
+            nuevo.classList.remove('desde-der', 'desde-izq'); void nuevo.offsetWidth;
+            nuevo.classList.add('desde-' + como.desde);
+            nuevo.addEventListener('animationend', function () { nuevo.classList.remove('desde-der', 'desde-izq'); }, { once: true });
+          }
+        }
+        if (como.comoMenu) irALista();
+      });
+    }
+
+    tabs.forEach(function (t, i) {
+      t.addEventListener('click', function () { elegir(o.id(t), { comoMenu: p > 0.5 }); });
+      t.addEventListener('keydown', function (e) {
+        var n = tabs.length;
+        var j = e.key === 'ArrowRight' ? (i + 1) % n : e.key === 'ArrowLeft' ? (i - 1 + n) % n
+              : e.key === 'Home' ? 0 : e.key === 'End' ? n - 1 : -1;
+        if (j >= 0) { e.preventDefault(); elegir(o.id(tabs[j]), { foco: true }); }
+      });
+    });
+    if (o.inicio) o.inicio.addEventListener('click', function (e) { e.preventDefault(); ir(0, true); });
+
+    // ── deslizar entre pestañas (como un PageView) ──
+    var zona = o.zonaGesto;
+    if (zona) {
+      var vecino = function (dir) { var j = indice() + dir; return (j >= 0 && j < tabs.length) ? o.id(tabs[j]) : null; };
+      var corre = function (el) {
+        for (; el && el !== zona; el = el.parentElement) {
+          var ox = getComputedStyle(el).overflowX;
+          if ((ox === 'auto' || ox === 'scroll') && el.scrollWidth > el.clientWidth + 2) return el;
+        }
+        return null;
+      };
+      var x0 = null, y0 = 0, dx = 0, eje = null, panel = null, rueda = null;
+      zona.addEventListener('touchstart', function (e) {
+        if (e.touches.length !== 1) { x0 = null; return; }
+        rueda = corre(e.target);
+        x0 = e.touches[0].clientX; y0 = e.touches[0].clientY; dx = 0; eje = null; panel = o.panel();
+      }, { passive: true });
+      zona.addEventListener('touchmove', function (e) {
+        if (x0 === null || !panel) return;
+        var mx = e.touches[0].clientX - x0, my = e.touches[0].clientY - y0;
+        if (!eje) {
+          if (Math.abs(mx) < 10 && Math.abs(my) < 10) return;
+          eje = Math.abs(mx) > Math.abs(my) * 1.3 ? 'x' : 'y';
+          // como un PageView anidado: el que se desplaza de costado manda, salvo que esté en su borde
+          if (eje === 'x' && rueda) {
+            var finD = rueda.scrollLeft + rueda.clientWidth >= rueda.scrollWidth - 2, finI = rueda.scrollLeft <= 2;
+            if (!(mx < 0 ? finD : finI)) eje = 'rueda';
+          }
+        }
+        if (eje !== 'x') return;
+        // el gesto horizontal es de la página: sin esto el navegador lo toma como «volver atrás»
+        // (en gioser.net el que se desplaza es un `.layer` fijo, donde overscroll-behavior no alcanza)
+        if (e.cancelable) e.preventDefault();
+        dx = vecino(mx < 0 ? 1 : -1) ? mx : mx * 0.25;   // en el borde resiste, como el rebote
+        panel.style.transition = 'none';
+        panel.style.transform = 'translateX(' + (dx * 0.6).toFixed(1) + 'px)';
+        panel.style.opacity = (1 - Math.min(0.45, Math.abs(dx) / innerWidth)).toFixed(3);
+      }, { passive: false });
+      zona.addEventListener('touchend', function () {
+        if (x0 === null || !panel) return;
+        x0 = null;
+        var p0 = panel, d = dx, dir = d < 0 ? 1 : -1, id = vecino(dir);
+        p0.style.transition = 'transform .2s ease, opacity .2s ease';
+        p0.style.transform = ''; p0.style.opacity = '';
+        setTimeout(function () { p0.style.transition = ''; }, 220);
+        if (eje !== 'x' || !id || Math.abs(d) < Math.min(90, innerWidth * 0.22)) return;
+        elegir(id, { desde: dir > 0 ? 'der' : 'izq', comoMenu: p > 0.5 });
+      });
+    }
+
+    medir(); pintar();
+    // las fuentes y la portada llegan tarde y mueven las medidas (la primera vez trastabillaba)
+    if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { medir(); pintar(); });
+    addEventListener('load', function () { medir(); pintar(); });
+
+    return {
+      medir: medir, pintar: pintar, irALista: irALista,
+      p: function () { return p; },
+      elegir: function (id, como) { return elegir(id, como); }
+    };
+  }
+
+  window.Franja = Franja;
+})();
diff --git a/web/takana-os.net/index.html b/web/takana-os.net/index.html
index cfb0f90e..e7b31eb7 100644
--- a/web/takana-os.net/index.html
+++ b/web/takana-os.net/index.html
@@ -9,9 +9,13 @@
 <link rel="icon" href="simbolo.svg" type="image/svg+xml">
 <link rel="stylesheet" href="fuentes.css">
 <meta property="og:title" content="takana — la forja determinista">
-<meta property="og:description" content="Un laboratorio hermético abajo, un Linux clásico y mutable arriba. 941 recetas, 926 selladas.">
+<meta property="og:description" content="Un laboratorio hermético abajo, un Linux clásico y mutable arriba. 954 recetas, todas selladas.">
 <meta property="og:url" content="https://takana-os.net/">
 <meta property="og:type" content="website">
+<meta property="og:image" content="https://takana-os.net/tarjeta.jpg">
+<meta property="og:image:width" content="1200">
+<meta property="og:image:height" content="630">
+<meta name="twitter:card" content="summary_large_image">
 <style>
 /* ── paleta oficial (docs/marca/README.md) ─────────────────────────────────────
    Obsidiana #17130E · Piedra #2A241B · Alpaca #F2EBDD
@@ -951,126 +955,50 @@ html.js .t-panel.activo.desde-izq{animation:t-desde-izq .32s cubic-bezier(.2,.8,
 
 
 </script>
+<script src="franja.js"></script>
 <script>
-/* ── la franja y las pestañas (2026-10-13) ──────────────────────────────────────────────────────
-   El símbolo sube desde la portada con el primer scroll y se planta en la franja; cuando las
-   pestañas de las secciones llegan arriba, se encogen y se acomodan en ella: el menú son ellas.
-   Elegir una muestra su sección; sin JS se ven las cuatro, una abajo de otra. */
+/* ── las pestañas de las cuatro secciones ───────────────────────────────────────────────────────
+   La franja —el símbolo que sube desde la portada, las pestañas que se vuelven el menú, el gesto de
+   deslizar entre secciones y la señal de lectura— es `franja.js`, la misma de gioser.net y
+   tawasuyu.net. Es una COPIA de la de gioser-web (allí `scripts/franja-copias.sh` compara las
+   tres): este sitio no le pide nada a terceros. Acá queda lo de esta página: mostrar la sección
+   elegida, el `#` de la dirección y los enlaces internos. Sin JS se ven las cuatro. */
 (function(){
- var r=document.documentElement, quieto=matchMedia('(prefers-reduced-motion: reduce)').matches;
- r.classList.add('js');
- var franja=document.getElementById('t-franja'), marca=document.querySelector('#t-marca .simbolo'),
-     grande=document.querySelector('.t-tarjeta .titular .simbolo'), glifos=document.getElementById('t-glifos'),
-     cuerpo=document.querySelector('.t-cuerpo'), barra=document.getElementById('t-tabs'),
-     tabs=[].slice.call(document.querySelectorAll('.t-tab')), tarjeta=document.querySelector('.t-tarjeta');
- if(!franja||!barra||!cuerpo) return;
- var suave=function(t){return t*t*(3-2*t)}, RAMPA=90, geo=null, menu=null, p=0;
-
- function medir(){
-  var s=scrollY, alto=franja.offsetHeight;
-  if(marca&&grande){ marca.style.transform='none';
-   var h=grande.getBoundingClientRect(), b=marca.getBoundingClientRect();
-   geo={hx:h.left,hy:h.top+s,hw:h.width,bx:b.left,by:b.top,bw:b.width}; }
-  barra.style.setProperty('--p',0); tabs.forEach(function(t){t.style.transform='none'});
-  var top=alto-barra.offsetHeight; barra.style.setProperty('--t-tabs-top',top+'px');
-  // las pestañas son lo primero del cuerpo (el fondo vivo no ocupa lugar); un sticky reporta su
-  // posición pegada, así que se mide desde el cuerpo
-  var docTop=s+cuerpo.getBoundingClientRect().top, br=barra.getBoundingClientRect();
-  var rot=tabs.map(function(t){ var l=t.querySelector('.rot').getBoundingClientRect(), b=t.getBoundingClientRect();
-   return {cx:l.left+l.width/2,w:l.width,cy:l.top+l.height/2-br.top,ox:l.left+l.width/2-b.left,oy:l.top+l.height/2-b.top}; });
-  var izq=(marca?marca.getBoundingClientRect().right:0)+(innerWidth<720?14:110),
-      der=(glifos?glifos.getBoundingClientRect().left:innerWidth)-14, gap=innerWidth<720?12:22,
-      suma=rot.reduce(function(a,l){return a+l.w},0),
-      k=Math.max(.55,Math.min(.92,(der-izq-gap*(rot.length-1))/suma)), total=suma*k+gap*(rot.length-1),
-      x0=innerWidth/2-total/2;
-  if(x0<izq) x0=izq; if(x0+total>der) x0=Math.max(izq,der-total);
-  var x=x0; rot.forEach(function(l){ l.dx=x+l.w*k/2-l.cx; x+=l.w*k+gap; });
-  menu={alto:alto,top:top,pegado:docTop-top,rot:rot,k:k};
-  tabs.forEach(function(t,i){ t.style.transformOrigin=rot[i].ox+'px '+rot[i].oy+'px'; });
- }
- function volar(s){ if(!geo) return;
-  var t=Math.min(1,Math.max(0,s/Math.max(1,geo.hy-geo.by))), e=suave(t),
-      x=geo.hx+(geo.bx-geo.hx)*e, y=Math.max(geo.by,geo.hy-s), k=geo.hw/geo.bw+(1-geo.hw/geo.bw)*e;
-  marca.style.transform='translate('+(x-geo.bx).toFixed(1)+'px,'+(y-geo.by).toFixed(1)+'px) scale('+k.toFixed(3)+')';
-  franja.classList.toggle('llego',t>.98); }
- function encoger(s){ if(!menu) return;
-  var t=Math.min(1,Math.max(0,(s-(menu.pegado-RAMPA))/RAMPA)); p=suave(t);
-  barra.style.setProperty('--p',p.toFixed(3));
-  var falta=Math.max(0,menu.pegado-s);
-  tabs.forEach(function(tab,i){ var l=menu.rot[i], dy=menu.alto/2-(menu.top+l.cy+falta), k=1-(1-menu.k)*p;
-   tab.style.transform = p===0 ? 'none' : 'translate('+(l.dx*p).toFixed(1)+'px,'+(dy*p).toFixed(1)+'px) scale('+k.toFixed(3)+')'; });
-  franja.classList.toggle('con-menu',p>.5); }
- var tick=false;
- function pintar(){ tick=false; var s=scrollY, alto=franja.offsetHeight; volar(s); encoger(s);
-  if(tarjeta){ var tp=tarjeta.getBoundingClientRect().top; tarjeta.style.opacity=Math.min(1,Math.max(0,(tp-alto)/(alto*1.5))).toFixed(3); }
-  franja.classList.toggle('sobre-portada', cuerpo.getBoundingClientRect().top>alto); }
- addEventListener('scroll',function(){ if(!tick){tick=true;requestAnimationFrame(pintar)} },{passive:true});
- addEventListener('resize',function(){medir();pintar()},{passive:true});
- medir(); pintar();
- if(document.fonts&&document.fonts.ready) document.fonts.ready.then(function(){medir();pintar()});
- // y otra vez al terminar de cargar todo: la primera vez trastabillaba porque se medía antes
- addEventListener('load',function(){medir();pintar()});
- document.getElementById('t-marca').addEventListener('click',function(e){ e.preventDefault(); scrollTo({top:0,behavior:quieto?'auto':'smooth'}); });
-
- function elegir(id, foco){
+ var cuerpo=document.querySelector('.t-cuerpo'), barra=document.getElementById('t-tabs');
+ var tabs=[].slice.call(document.querySelectorAll('.t-tab'));
+ if(!cuerpo||!barra||!window.Franja) return;
+ function elegir(id){
   // sin un id que sea de una pestaña no se toca nada (al cargar sin ancla, «Descargar» queda)
   if(!tabs.some(function(t){return t.dataset.tab===id})) return false;
-  var hay=false;
-  tabs.forEach(function(t){ var on=t.dataset.tab===id; if(on) hay=true;
+  tabs.forEach(function(t){ var on=t.dataset.tab===id;
    t.setAttribute('aria-selected',String(on)); t.tabIndex=on?0:-1;
-   var panel=document.getElementById(t.dataset.tab); if(panel) panel.classList.toggle('activo',on);
-   if(on&&foco) t.focus(); });
-  if(hay) try{ history.replaceState(null,'','#'+id); }catch(e){}
-  return hay;
+   var panel=document.getElementById(t.dataset.tab); if(panel) panel.classList.toggle('activo',on); });
+  try{ history.replaceState(null,'','#'+id); }catch(e){}
+  return true;
  }
- tabs.forEach(function(t,i){
-  t.tabIndex = t.getAttribute('aria-selected')==='true' ? 0 : -1;
-  t.addEventListener('click',function(){ var comoMenu=p>.5; elegir(t.dataset.tab);
-   if(comoMenu&&menu) scrollTo({top:Math.round(menu.pegado),behavior:quieto?'auto':'smooth'}); });
-  t.addEventListener('keydown',function(e){ var n=tabs.length,
-   j=e.key==='ArrowRight'?(i+1)%n:e.key==='ArrowLeft'?(i-1+n)%n:e.key==='Home'?0:e.key==='End'?n-1:-1;
-   if(j>=0){ e.preventDefault(); elegir(tabs[j].dataset.tab,true); } });
+ tabs.forEach(function(t){ t.tabIndex = t.getAttribute('aria-selected')==='true' ? 0 : -1; });
+ var franja=window.Franja({
+  franja:document.getElementById('t-franja'),
+  marca:document.querySelector('#t-marca .simbolo'),
+  grande:document.querySelector('.t-tarjeta .titular .simbolo'),
+  glifos:document.getElementById('t-glifos'),
+  cuerpo:cuerpo,
+  inicio:document.getElementById('t-marca'),
+  barra:barra, tabs:tabs, rotulo:'.rot', varTop:'--t-tabs-top', margen:110,
+  apagar:[document.querySelector('.t-tarjeta')],
+  inicioBarra:function(){ return cuerpo.getBoundingClientRect().top; },
+  zonaGesto:cuerpo, senal:true,
+  id:function(t){ return t.dataset.tab; },
+  panel:function(){ return document.querySelector('.t-panel.activo'); },
+  elegir:elegir
  });
-
- // ── deslizar entre pestañas (como un PageView de Flutter) ──
- function vecino(dir){ var i=tabs.findIndex(function(t){return t.getAttribute('aria-selected')==='true'}), j=i+dir;
-  return (j>=0&&j<tabs.length) ? tabs[j].dataset.tab : null; }
- var x0=null, y0=0, dx=0, eje=null, panel=null, rueda=null;
- // lo que bajo el dedo se desplaza de costado (un diagrama, una tabla ancha, un bloque de código)
- function corre(el){ for(;el&&el!==cuerpo;el=el.parentElement){ var o=getComputedStyle(el).overflowX;
-  if((o==='auto'||o==='scroll')&&el.scrollWidth>el.clientWidth+2) return el; } return null; }
- cuerpo.addEventListener('touchstart',function(e){ if(e.touches.length!==1){x0=null;return;}
-  rueda=corre(e.target);
-  x0=e.touches[0].clientX; y0=e.touches[0].clientY; dx=0; eje=null; panel=document.querySelector('.t-panel.activo'); },{passive:true});
- cuerpo.addEventListener('touchmove',function(e){ if(x0===null||!panel) return;
-  var mx=e.touches[0].clientX-x0, my=e.touches[0].clientY-y0;
-  if(!eje){ if(Math.abs(mx)<10&&Math.abs(my)<10) return; eje=Math.abs(mx)>Math.abs(my)*1.3?'x':'y';
-   // como un PageView anidado: el que se desplaza de costado manda, salvo que ya esté en su borde
-   if(eje==='x'&&rueda){ var finD=rueda.scrollLeft+rueda.clientWidth>=rueda.scrollWidth-2, finI=rueda.scrollLeft<=2;
-    if(!(mx<0?finD:finI)) eje='rueda'; } }
-  if(eje!=='x') return;
-  if(e.cancelable) e.preventDefault();          // el gesto horizontal es de la página, no «volver atrás»
-  dx = vecino(mx<0?1:-1) ? mx : mx*.25;          // en el borde resiste, como el rebote
-  panel.style.transition='none'; panel.style.transform='translateX('+(dx*.6).toFixed(1)+'px)';
-  panel.style.opacity=(1-Math.min(.45,Math.abs(dx)/innerWidth)).toFixed(3); },{passive:false});
- cuerpo.addEventListener('touchend',function(){ if(x0===null||!panel) return; x0=null;
-  var p0=panel, d=dx, dir=d<0?1:-1, id=vecino(dir);
-  p0.style.transition='transform .2s ease,opacity .2s ease'; p0.style.transform=''; p0.style.opacity='';
-  setTimeout(function(){p0.style.transition=''},220);
-  if(eje!=='x'||!id||Math.abs(d)<Math.min(90,innerWidth*.22)) return;
-  elegir(id);
-  var nuevo=document.getElementById(id); nuevo.classList.remove('desde-der','desde-izq'); void nuevo.offsetWidth;
-  nuevo.classList.add(dir>0?'desde-der':'desde-izq');
-  nuevo.addEventListener('animationend',function(){nuevo.classList.remove('desde-der','desde-izq')},{once:true});
-  if(p>.5&&menu) scrollTo({top:Math.round(menu.pegado),behavior:quieto?'auto':'smooth'}); });
  // un enlace con ancla a una sección (#codigo, #descargar…) abre esa pestaña
  var pedido=decodeURIComponent((location.hash||'').slice(1));
- if(elegir(pedido)&&menu) scrollTo({top:Math.round(menu.pegado)});
- addEventListener('hashchange',function(){ var id=decodeURIComponent((location.hash||'').slice(1));
-  if(elegir(id)&&menu) scrollTo({top:Math.round(menu.pegado),behavior:quieto?'auto':'smooth'}); });
+ if(elegir(pedido)) franja.irALista();
+ addEventListener('hashchange',function(){ if(elegir(decodeURIComponent((location.hash||'').slice(1)))) franja.irALista(); });
  document.addEventListener('click',function(e){ var a=e.target.closest('a[href^="#"]'); if(!a) return;
   var id=a.getAttribute('href').slice(1); if(tabs.some(function(t){return t.dataset.tab===id})){
-   e.preventDefault(); elegir(id); if(menu) scrollTo({top:Math.round(menu.pegado),behavior:quieto?'auto':'smooth'}); } });
+   e.preventDefault(); elegir(id); franja.irALista(); } });
 })();
 </script>
 </body>
diff --git a/web/takana-os.net/tarjeta.jpg b/web/takana-os.net/tarjeta.jpg
new file mode 100644
index 00000000..22b7c1ca
Binary files /dev/null and b/web/takana-os.net/tarjeta.jpg differ