<?php
/* Página /catalog/: sección normal, sin padding, ancho 100%, sin animaciones S7. Mantener header/footer.
Componente IDÉNTICO al usado en la sección S7 de la home — mismas pilas, mismo listado, mismo radio,
misma alineación de títulos, mismo arreglo de la zona segura bajo el header. Única diferencia real:
OWNS_HEADER_BASELINE (abajo, en el script) es true aquí porque esta página gestiona su propio header. */
$mkc_items = [];
if (function_exists('wc_get_products')) {
$mkc_args = [
'status' => 'publish',
'limit' => -1,
'visibility' => 'catalog',
'orderby' => 'date',
'order' => 'ASC',
'return' => 'objects',
];
if ('yes' === get_option('woocommerce_hide_out_of_stock_items')) {
$mkc_args['stock_status'] = 'instock';
}
foreach (wc_get_products($mkc_args) as $mkc_product) {
$mkc_image_id = $mkc_product->get_image_id();
$mkc_items[] = [
'id' => $mkc_product->get_id(),
'title' => wp_specialchars_decode($mkc_product->get_name(), ENT_QUOTES),
'url' => $mkc_product->get_permalink(),
'image' => $mkc_image_id ? (wp_get_attachment_image_url($mkc_image_id, 'large') ?: '') : '',
'category' => 'products',
'canvas' => true,
];
}
}
foreach (['system' => 'systems', 'customs' => 'custom'] as $post_type => $category) {
$mkc_query = new WP_Query([
'post_type' => $post_type, 'post_status' => 'publish',
'posts_per_page' => -1, 'orderby' => 'date', 'order' => 'ASC',
'no_found_rows' => true,
]);
foreach ($mkc_query->posts as $mkc_post) {
$mkc_items[] = [
'id' => $mkc_post->ID,
'title' => wp_specialchars_decode(get_the_title($mkc_post->ID), ENT_QUOTES),
'url' => get_permalink($mkc_post->ID),
'image' => get_the_post_thumbnail_url($mkc_post->ID, 'large') ?: '',
'category' => $category,
'canvas' => true,
];
}
}
?>
<style>
#mkc { --mkc-header-h:80px; --mkc-radius:24px; position:relative; isolation:isolate; width:100%; height:100vh; height:100svh; min-height:540px; overflow:hidden; background:var(--secondary,#E8E3D6); color:var(--primary,#232220); }
#mkc, #mkc * { box-sizing:border-box; }
#mkc button, #mkc a { -webkit-tap-highlight-color:transparent; }
#mkc a { color:inherit; text-decoration:none; }
#mkc button:focus-visible, #mkc a:focus-visible { outline:2px solid currentColor; outline-offset:5px; }
#mkc-stage { position:absolute; inset:0; overflow:hidden; touch-action:pan-y; }
#mkc-canvas { position:absolute; left:50%; top:0; will-change:transform; transition:opacity .35s; }
.mkc-card { position:absolute; display:block; transform-origin:center; transition:transform .35s; }
.mkc-card img, .mkc-result img { display:block; width:100%; aspect-ratio:16/9; object-fit:cover; background:rgba(35,34,32,.08); pointer-events:none; }
.mkc-card img { border-radius:var(--mkc-radius); }
.mkc-title { display:block; margin-top:var(--space-2xs,8px); font-family:'AntartidaRounded','Antartida Rounded',sans-serif; font-size:var(--text-l,20px); font-weight:300; font-style:italic; letter-spacing:.05em; line-height:1.2; }
.mkc-card .mkc-title { text-align:center; }
.mkc-result .mkc-title { text-align:right; font-size:20px; }
@media(hover:hover) and (pointer:fine) { .mkc-card:hover { transform:scale(1.5); z-index:2; } }
#mkc-nav { position:absolute; right:44px; bottom:36px; z-index:5; display:flex; flex-direction:column; align-items:flex-end; }
#mkc-nav button { appearance:none; background:none; border:0; border-radius:0; padding:0; margin:0; color:inherit; font-family:'Neue Haas Grotesk Display Pro',sans-serif; font-weight:900; font-size:clamp(30px,3.4vw,54px); line-height:1; letter-spacing:-.02em; text-align:right; cursor:pointer; }
#mkc-nav button:hover { opacity:.65; }
#mkc-nav button[aria-pressed="true"] { text-decoration:underline; text-decoration-thickness:2px; text-underline-offset:5px; }
#mkc-nav .mkc-all { font-size:14px; font-weight:400; letter-spacing:0; margin-bottom:12px; }
#mkc-view { position:absolute; inset:0; z-index:3; }
#mkc-view[hidden], #mkc .mkc-all[hidden] { display:none; }
#mkc-copy { position:absolute; z-index:1; left:48px; top:0; bottom:0; width:42%; display:flex; flex-direction:column; justify-content:center; padding-top:var(--mkc-header-h); min-width:0; }
#mkc-heading { margin:0 0 24px; font-family:'Neue Haas Grotesk Display Pro',sans-serif; font-size:clamp(36px,4.5vw,64px); font-weight:900; line-height:1; letter-spacing:-.02em; text-transform:uppercase; }
#mkc-desc { margin:0; max-width:340px; font-family:'AntartidaRounded','Antartida Rounded',sans-serif; font-size:clamp(16px,1.7vw,24px); font-weight:300; line-height:1.55; }
#s7-cat-right { position:absolute; z-index:2; inset:0; display:grid; grid-template-columns:minmax(0,1fr); align-content:start; gap:24px; overflow-x:hidden; overflow-y:auto; padding:calc(var(--mkc-header-h) + 24px) 44px var(--mkc-nav-h,210px) calc(42% - 44px); overscroll-behavior-x:contain; overscroll-behavior-y:auto; scrollbar-width:none; -ms-overflow-style:none; }
#s7-cat-right::-webkit-scrollbar { display:none; width:0; height:0; }
#s7-cat-right:focus-visible { outline:none; }
#s7-cat-right a:focus-visible { outline-offset:-3px; }
.mkc-result { display:block; min-width:0; }
.mkc-result img { border-radius:var(--mkc-radius); }
#mkc-empty { position:absolute; inset:45% 24px auto; text-align:center; }
#brx-header.mkc-header { background:var(--secondary,#E8E3D6) !important; transition:background .3s; }
#brx-header.mkc-header.mkc-header--clear { background:transparent !important; }
#brx-header.mkc-header a { color:var(--primary,#232220) !important; }
#brx-header.mkc-header #brxe-qzydyo, #brx-header.mkc-header #brxe-mzyxsm,
#brx-header.mkc-header #brxe-ubwcas,
#brx-header.mkc-header [class*="cart"] img,
#brx-header.mkc-header [class*="burger"], #brx-header.mkc-header [class*="toggle"] { filter:brightness(0) !important; }
#brx-header.mkc-header svg { color:var(--primary,#232220) !important; }
#brx-header.mkc-header svg [fill]:not([fill="none"]) { fill:var(--primary,#232220) !important; }
#brx-header.mkc-header svg [stroke]:not([stroke="none"]) { stroke:var(--primary,#232220) !important; }
@media(max-width:767px) {
#mkc { min-height:520px; }
.mkc-title { font-size:var(--text-s,14px); }
#mkc-nav { right:24px; bottom:24px; }
#mkc-nav button { font-size:clamp(24px,7vw,40px); }
#mkc-view { display:flex; flex-direction:column; }
#mkc-copy { position:relative; left:auto; top:auto; bottom:auto; width:auto; flex:none; justify-content:flex-start; padding:calc(var(--mkc-header-h) + 16px) 24px 0; }
#mkc-heading { font-size:32px; margin-bottom:12px; text-transform:none; }
#mkc-desc { font-size:13px; max-width:100%; }
#s7-cat-right { position:relative; inset:auto; flex:1; min-height:0; padding:20px 24px var(--mkc-nav-h,170px); }
.mkc-result .mkc-title { font-size:14px; }
}
@media(prefers-reduced-motion:reduce) { #mkc *, #brx-header.mkc-header { transition:none !important; } }
</style>
<div id="mkc" role="region" aria-label="Mokoto catalog">
<div id="mkc-stage"><div id="mkc-canvas"></div></div>
<div id="mkc-view" hidden>
<div id="mkc-copy"><h1 id="mkc-heading"></h1><p id="mkc-desc"></p></div>
<div id="s7-cat-right" tabindex="0" aria-label="Category designs"></div>
</div>
<nav id="mkc-nav" aria-label="Catalog categories">
<button class="mkc-all" type="button" hidden>All designs</button>
<button type="button" data-cat="systems" aria-pressed="false">systems</button>
<button type="button" data-cat="products" aria-pressed="false">products</button>
<button type="button" data-cat="custom" aria-pressed="false">custom design</button>
</nav>
<p id="mkc-empty" hidden>No designs available yet.</p>
</div>
<script type="application/json" id="mkc-data"><?php echo wp_json_encode($mkc_items, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?></script>
<script>
(function () {
const OWNS_HEADER_BASELINE = true;
const FX = {
gather: 900, gatherStep: 70, hold: 800,
unstack: 1000, unstackStep: 160, perStack: 5
};
const STACK_ORDER = ['systems','products','custom'];
function init() {
const root = document.getElementById('mkc');
if (!root || root.dataset.ready) return;
root.dataset.ready = '1';
const dataEl = document.getElementById('mkc-data');
const data = dataEl ? JSON.parse(dataEl.textContent) : [];
const pool = data.filter(item => item.canvas !== false);
const stage = root.querySelector('#mkc-stage'), canvas = root.querySelector('#mkc-canvas');
const view = root.querySelector('#mkc-view'), results = document.getElementById('s7-cat-right');
const copy = root.querySelector('#mkc-copy');
const nav = root.querySelector('#mkc-nav'), all = nav.querySelector('.mkc-all');
const buttons = [...nav.querySelectorAll('[data-cat]')];
const header = document.getElementById('brx-header');
if (header && OWNS_HEADER_BASELINE) header.classList.add('mkc-header');
const motion = matchMedia('(prefers-reduced-motion: reduce)');
const meta = {
systems: ['Systems','Discover our systems, they are design patterns developed as scalable frameworks. Our systems can be tailored to your space, while preserving its core design language.'],
products: ['Products','Our proprietary product line redefines the relationship between object and user through bold, multifunctional design.'],
custom: ['Custom design','We approach the creation of collective spaces, hotels, and retail environments from a 360° perspective, taking ownership of the entire process.']
};
let active = null, animations = [], gatherAnims = [], busy = false, raf = 0, visible = true;
let headerOverlap = false, headerH = 0;
let x = 0, y = 0, tx = 0, ty = 0, maxX = 0, maxY = 0;
let width = 0, height = 0;
const clamp = (n,a,b) => Math.max(a,Math.min(b,n));
const ease = 'cubic-bezier(.16,1,.3,1)';
window.s7State = window.s7State || {};
window.s7State.resetToScatter = () => render(null, true, null);
function makeCard(item, className) {
const a = document.createElement('a');
a.className = className; a.href = item.url;
a.dataset.id = item.id; a.dataset.cat = item.category;
const img = document.createElement('img');
img.alt = ''; img.loading = 'lazy'; img.draggable = false;
if (item.image) img.src = item.image;
const title = document.createElement('span'); title.className = 'mkc-title';
title.textContent = item.title.charAt(0).toUpperCase() + item.title.slice(1).toLowerCase();
a.append(img,title); a.draggable = false;
return a;
}
function transform() { canvas.style.transform = `translate(calc(-50% + ${x}px), ${y}px)`; }
function stop() { cancelAnimationFrame(raf); raf = 0; }
function tick() {
raf = 0;
if (active || busy || !visible || document.hidden) return;
x += (tx-x)*.06; y += (ty-y)*.06;
if (Math.abs(tx-x)<.1 && Math.abs(ty-y)<.1) { x=tx; y=ty; transform(); return; }
transform(); raf = requestAnimationFrame(tick);
}
function move() {
if (active || busy || !visible || document.hidden) return;
if (motion.matches) { x=tx; y=ty; transform(); }
else if (!raf) raf = requestAnimationFrame(tick);
}
function measureChrome() {
const r = root.getBoundingClientRect();
const h = header ? Math.max(0, header.getBoundingClientRect().bottom - r.top) : 0;
headerOverlap = h > 0;
headerH = Math.min(h,160);
root.style.setProperty('--mkc-header-h', headerH+'px');
root.style.setProperty('--mkc-nav-h',(nav.offsetHeight + (width<768 ? 48 : 60))+'px');
}
function syncHeader() {
if (header && OWNS_HEADER_BASELINE) header.classList.toggle('mkc-header--clear', Boolean(active) && headerOverlap);
}
function layout() {
const w = root.clientWidth, h = root.clientHeight;
measureChrome();
if (w===width && h===height) return;
width=w; height=h; stop(); canvas.replaceChildren(); gatherAnims=[];
const mobile=w<768, cardW=mobile?220:390;
const cellW=mobile?280:624, cellH=Math.max(h/3,cardW*9/16+60);
const cols=Math.max(mobile?8:14,Math.ceil(pool.length/2));
const cw=cols*cellW, ch=4*cellH;
canvas.style.width=cw+'px'; canvas.style.height=ch+'px';
const cells=[];
for (let row=0;row<4;row++) for (let col=0;col<cols;col++) {
if ((row+col)%2!==1) continue;
cells.push({row,col,cx:col*cellW+cellW/2,cy:row*cellH+cellH/2,item:-1});
}
if (pool.length) {
let seed=42;
const rand=()=>{ seed=(Math.imul(seed,1664525)+1013904223)>>>0; return seed/4294967296; };
const order=[...pool.keys()];
for (let i=order.length-1;i>0;i--) { const j=Math.floor(rand()*(i+1)); [order[i],order[j]]=[order[j],order[i]]; }
const midX=cw/2, midY=ch/2;
const byCenter=[...cells].sort((a,b)=>Math.hypot((a.cx-midX)/w,(a.cy-midY)/h)-Math.hypot((b.cx-midX)/w,(b.cy-midY)/h));
const placed=pool.map(()=>[]);
byCenter.forEach(cell=>{
let best=order[0], bestDist=-1, bestCount=Infinity;
order.forEach(p=>{
let d=Infinity;
for (const q of placed[p]) d=Math.min(d,Math.hypot((q.cx-cell.cx)/w,(q.cy-cell.cy)/h));
const c=placed[p].length;
if (d>bestDist+1e-6 || (d>=bestDist-1e-6 && c<bestCount)) { best=p; bestDist=d; bestCount=c; }
});
cell.item=best; placed[best].push(cell);
});
cells.forEach(cell=>{
const card=makeCard(pool[cell.item],'mkc-card');
card.style.width=cardW+'px';
card.style.left=(cell.col*cellW+(cellW-cardW)/2)+'px';
card.style.top=(cell.row*cellH+(cellH-cardW*9/16-32)/2)+'px';
canvas.append(card);
});
}
maxX=Math.max(0,(cw-w)/2); maxY=Math.max(0,ch-h);
x=tx=0; y=ty=-maxY/2; transform(); measureChrome();
}
function cancelGather() {
gatherAnims.forEach(a=>a.cancel()); gatherAnims=[];
canvas.querySelectorAll('.mkc-card').forEach(el=>{ el.style.transformOrigin=''; el.style.zIndex=''; });
}
function gather(cat) {
const cards=[...canvas.querySelectorAll('.mkc-card')];
if (!cards.length) return null;
const r=root.getBoundingClientRect(), w=r.width, h=r.height;
const pad=w<768?24:48, gapRatio=.13;
const stackW=Math.min(cards[0].offsetWidth*.84,(w-2*pad)/(3+2*gapRatio));
const gap=stackW*gapRatio, stackH=stackW*9/16;
const total=3*stackW+2*gap, left0=r.left+(w-total)/2, cy=r.top+h/2;
const slots={};
STACK_ORDER.forEach((k,i)=>slots[k]={cx:left0+i*(stackW+gap)+stackW/2,cy,cards:[]});
const onScreen=b=>b.right>r.left-w*.15 && b.left<r.right+w*.15 && b.bottom>r.top-h*.15 && b.top<r.bottom+h*.15;
cards.forEach(el=>{
const s=slots[el.dataset.cat]; if (!s) return;
const b=el.querySelector('img').getBoundingClientRect();
const c={el,x:b.left+b.width/2,y:b.top+b.height/2};
c.d=Math.hypot(c.x-s.cx,c.y-s.cy); c.on=onScreen(b);
s.cards.push(c);
});
STACK_ORDER.forEach(k=>{
const s=slots[k];
s.cards.sort((a,b)=>a.d-b.d);
const on=s.cards.filter(c=>c.on);
s.cards=on.length ? on.slice(0,FX.perStack) : s.cards.slice(0,1);
});
if (!slots[cat].cards.length) return null;
cancelGather();
let seed=7;
const rand=()=>{ seed=(Math.imul(seed,1664525)+1013904223)>>>0; return seed/4294967296; };
const firstItem=data.find(item=>item.category===cat);
const firstId=firstItem ? String(firstItem.id) : '';
const chosen=new Set();
STACK_ORDER.forEach(k=>{
const s=slots[k];
if (k===cat) { const i=s.cards.findIndex(c=>c.el.dataset.id===firstId); if (i>-1) s.cards.push(s.cards.splice(i,1)[0]); }
s.cards.forEach((c,i)=>{
const el=c.el, img=el.querySelector('img'), title=el.querySelector('.mkc-title');
chosen.add(el);
const sc=stackW/img.offsetWidth;
const dx=s.cx-c.x, dy=s.cy-c.y;
const jx=(rand()-.5)*28, jy=(rand()-.5)*18, rot=(rand()-.5)*5;
const delay=i*FX.gatherStep+rand()*60;
el.style.transformOrigin=`50% ${img.offsetHeight/2}px`;
el.style.zIndex=String(10+i);
gatherAnims.push(el.animate([
{transform:'translate(0px,0px) scale(1) rotate(0deg)'},
{transform:`translate(${dx+jx}px,${dy+jy}px) scale(${sc}) rotate(${rot}deg)`,offset:.78},
{transform:`translate(${dx}px,${dy}px) scale(${sc}) rotate(0deg)`}
],{duration:FX.gather,delay,easing:'cubic-bezier(.65,0,.35,1)',fill:'both'}));
if (title) gatherAnims.push(title.animate([{opacity:1},{opacity:0}],{duration:250,delay,fill:'both'}));
});
});
cards.forEach(el=>{ if (!chosen.has(el)) gatherAnims.push(el.animate([{opacity:1},{opacity:0}],{duration:350,fill:'forwards'})); });
const img0=slots[cat].cards[0].el.querySelector('img');
const radius=(parseFloat(getComputedStyle(img0).borderTopLeftRadius)||0)*(stackW/img0.offsetWidth);
const rect={left:slots[cat].cx-stackW/2, top:cy-stackH/2, width:stackW, height:stackH};
const done=Promise.all(gatherAnims.map(a=>a.finished)).then(()=>new Promise(res=>setTimeout(res,FX.hold)));
return {rect,radius,done};
}
function unstack(stack) {
const box=results.getBoundingClientRect();
const items=[...results.querySelectorAll('.mkc-result')];
const touched=[];
items.forEach((el,i)=>{
const end=el.getBoundingClientRect();
if (!end.width || end.top>=box.bottom) return;
const sc=stack.rect.width/end.width;
const delay=touched.length*FX.unstackStep;
el.style.zIndex=String(items.length-i); touched.push(el);
animations.push(el.animate([
{transform:`translate(${stack.rect.left-end.left}px,${stack.rect.top-end.top}px) scale(${sc})`,transformOrigin:'top left'},
{transform:'translate(0px,0px) scale(1)',transformOrigin:'top left'}
],{duration:FX.unstack,delay,easing:ease,fill:'backwards'}));
const img=el.querySelector('img'), title=el.querySelector('.mkc-title');
if (img) {
const endRadius=getComputedStyle(img).borderTopLeftRadius || '24px';
animations.push(img.animate([{borderRadius:(stack.radius/sc)+'px'},{borderRadius:endRadius}],{duration:FX.unstack,delay,easing:ease,fill:'backwards'}));
}
if (title) animations.push(title.animate([{opacity:0},{opacity:1}],{duration:400,delay:delay+FX.unstack*.55,fill:'backwards'}));
});
animations.push(copy.animate([{opacity:0,transform:'translateY(16px)'},{opacity:1,transform:'none'}],{duration:700,delay:250,easing:ease,fill:'backwards'}));
results.style.overflow='visible';
results.style.clipPath='inset(-300vh -300vw 0 -300vw)';
const cleanup=()=>{ results.style.overflow=''; results.style.clipPath=''; touched.forEach(el=>el.style.zIndex=''); };
Promise.all(animations.map(a=>a.finished)).then(cleanup,cleanup);
}
function setCategory(cat, updateURL=true, animate=false) {
if (!Object.prototype.hasOwnProperty.call(meta,cat)) cat=null;
if (busy) return;
if (active===cat && root.dataset.initialized) return;
root.dataset.initialized='1';
if (cat && !active && animate && !motion.matches && visible && !document.hidden) {
stop();
const stack=gather(cat);
if (stack) {
busy=true; stage.style.pointerEvents='none'; stage.inert=true;
buttons.forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.cat===cat)));
stack.done.catch(()=>{}).then(()=>{ busy=false; render(cat,updateURL,stack); });
return;
}
}
render(cat,updateURL,null);
}
function render(cat, updateURL, stack) {
const wasCategory=Boolean(active);
const origins=new Map();
if (!wasCategory && !stack) canvas.querySelectorAll('.mkc-card').forEach(el=> {
if (el.dataset.cat!==cat) return;
const rect=el.getBoundingClientRect();
const old=origins.get(el.dataset.id);
if (!old || Math.abs(rect.left-window.innerWidth/2)<Math.abs(old.left-window.innerWidth/2)) origins.set(el.dataset.id,rect);
});
if (!cat) cancelGather();
animations.forEach(a=>a.cancel()); animations=[];
active=cat; stop(); results.replaceChildren();
window.s7State.activeCategory = cat;
buttons.forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.cat===cat)));
all.hidden=!cat; view.hidden=!cat; stage.inert=Boolean(cat);
canvas.style.opacity=cat?'0':'1'; stage.style.pointerEvents=cat?'none':'';
root.querySelector('#mkc-empty').hidden=Boolean(cat) || Boolean(pool.length);
if (cat) {
root.querySelector('#mkc-heading').textContent=meta[cat][0];
root.querySelector('#mkc-desc').textContent=meta[cat][1];
const filtered=data.filter(item=>item.category===cat);
filtered.forEach(item=>results.append(makeCard(item,'mkc-result')));
if (!filtered.length) { const p=document.createElement('p'); p.textContent='No designs available in this category yet.'; results.append(p); }
results.scrollTop=0; measureChrome(); syncHeader();
if (!motion.matches) {
if (stack) unstack(stack);
else [...results.querySelectorAll('.mkc-result')].forEach((el,i)=> {
const end=el.getBoundingClientRect(), start=origins.get(el.dataset.id);
const from=start && end.width ? `translate(${start.left-end.left}px,${start.top-end.top}px) scale(${start.width/end.width})` : 'translateY(24px)';
animations.push(el.animate([
{transform:from,opacity:0,transformOrigin:'top left'},
{transform:'translate(0,0) scale(1)',opacity:1,transformOrigin:'top left'}
],{duration:1000,delay:Math.min(i,5)*100,easing:ease,fill:'backwards'}));
});
}
} else { measureChrome(); syncHeader(); move(); }
if (updateURL) {
const url=new URL(location.href); url.hash=cat || '';
history.replaceState(history.state,'',url);
}
}
results.addEventListener('wheel',e=>{
if (!active || busy || e.ctrlKey) return;
const top=root.getBoundingClientRect().top;
const unit=e.deltaMode===1 ? 16 : e.deltaMode===2 ? window.innerHeight : 1;
const dy=e.deltaY*unit;
if (dy<0 && top<-1) { e.preventDefault(); window.scrollBy({top:Math.max(dy,top),behavior:'instant'}); }
else if (dy>0 && top>1) { e.preventDefault(); window.scrollBy({top:Math.min(dy,top),behavior:'instant'}); }
},{passive:false});
buttons.forEach(b=>b.addEventListener('click',()=>setCategory(b.dataset.cat,true,true)));
all.addEventListener('click',()=>setCategory(null));
function readHash() { setCategory(location.hash.slice(1),false); }
window.addEventListener('hashchange',readHash);
window.addEventListener('pageshow',readHash);
stage.addEventListener('pointermove',e=> {
if (active || busy || e.pointerType!=='mouse' || drag) return;
const r=stage.getBoundingClientRect();
tx=clamp(-(e.clientX-r.left-r.width/2)/(r.width/2)*maxX,-maxX,maxX);
const safe=Math.min(headerH+16, maxY);
const raw=-maxY*clamp((e.clientY-r.top)/r.height,0,1);
ty=Math.min(raw,-safe);
move();
});
let drag=null, suppressClick=false;
stage.addEventListener('pointerdown',e=> {
if (active || busy || e.pointerType==='mouse' || !e.isPrimary) return;
drag={id:e.pointerId,start:e.clientX,base:tx,moved:false}; suppressClick=false;
});
stage.addEventListener('pointermove',e=> {
if (!drag || drag.id!==e.pointerId) return;
const delta=e.clientX-drag.start;
if (Math.abs(delta)>8) { drag.moved=true; stage.setPointerCapture(e.pointerId); }
tx=clamp(drag.base+delta,-maxX,maxX); move();
});
function endDrag(e) {
if (!drag || drag.id!==e.pointerId) return;
suppressClick=drag.moved; drag=null;
}
stage.addEventListener('pointerup',endDrag); stage.addEventListener('pointercancel',endDrag);
stage.addEventListener('click',e=> { if(suppressClick) { e.preventDefault(); e.stopPropagation(); suppressClick=false; } },true);
stage.addEventListener('focusin',e=> {
const card=e.target.closest('.mkc-card'); if (!card || active || busy) return;
tx=clamp(canvas.offsetWidth/2-card.offsetLeft-card.offsetWidth/2,-maxX,maxX);
ty=clamp(height/2-card.offsetTop-card.offsetHeight/2,-maxY,0);
move();
});
new ResizeObserver(layout).observe(root);
if(header) new ResizeObserver(measureChrome).observe(header);
new IntersectionObserver(entries=>{visible=entries[0].isIntersecting; if(visible)move();else stop();}).observe(root);
document.addEventListener('visibilitychange',()=>{if(document.hidden)stop();else move();});
root.querySelector('#mkc-empty').hidden=Boolean(pool.length);
layout(); readHash();
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init); else init();
})();
</script>