Initial commit: SolLunar Kitchen under version control

This commit is contained in:
Avi 2026-09-23 18:57:11 -05:00
commit 601c85f236
37 changed files with 6206 additions and 0 deletions

470
home.html Normal file
View file

@ -0,0 +1,470 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#FFFFFF">
<link rel="icon" type="image/png" sizes="64x64" href="icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="icon-512.png">
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png">
<link rel="manifest" href="manifest.webmanifest">
<title>K484 · A Private Membership Association</title>
<script>document.documentElement.style.colorScheme='light';try{localStorage.removeItem('kitchen484-theme');}catch(e){}</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
<style>
:root{
color-scheme:light;
--bg:#FFFFFF; --bg2:#f8fafc;
--ink:#0A0A0A; --ink2:#475569; --muted:#94a3b8;
--line:#e2e8f0; --line2:#cbd5e1;
--black:#0a0a0a;
--radius:18px;
--shadow:0 1px 2px rgb(10 10 10 / .05), 0 8px 24px -12px rgb(10 10 10 / .12);
--font:-apple-system,'Avenir Next','Avenir',system-ui,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
--display:'Orbitron',var(--font);
}
*{box-sizing:border-box;margin:0;padding:0}
html{line-height:1.5;scroll-behavior:smooth}
body{font-family:var(--font);background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}
a{color:inherit}
.wrap{max-width:1040px;margin:0 auto;padding:0 22px}
/* ============ HEADER ============ */
header{position:sticky;top:0;z-index:50;background:rgba(255,255,255,.9);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.header-in{display:flex;align-items:center;gap:12px;padding:12px 22px;max-width:1040px;margin:0 auto}
.logo{width:46px;height:46px;flex:0 0 46px;object-fit:contain}
.brand b{font-family:var(--display);font-weight:900;font-size:15px;letter-spacing:2px;display:block;line-height:1.2}
.brand span{font-size:11px;color:var(--muted);letter-spacing:1px}
.header-right{margin-left:auto;display:flex;align-items:center;gap:10px}
.member-chip{font-family:var(--display);font-size:10px;font-weight:700;letter-spacing:1px;border:1px solid var(--line2);border-radius:99px;padding:6px 12px;color:var(--ink2)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:none;cursor:pointer;font-family:var(--font);font-weight:700;font-size:14px;border-radius:99px;padding:12px 22px;transition:transform .12s,opacity .12s;text-decoration:none}
.btn:active{transform:scale(.97)}
.btn-primary{background:var(--black);color:#fff}
.btn-primary:hover{opacity:.88}
.btn-ghost{background:var(--bg);color:var(--ink);border:1.5px solid var(--line2)}
.btn-ghost:hover{border-color:var(--ink)}
.btn-sm{padding:9px 16px;font-size:13px}
/* ============ HERO ============ */
.hero{text-align:center;padding:72px 22px 56px;position:relative;overflow:hidden}
.hero-logo{width:140px;height:140px;object-fit:contain;margin:0 auto 26px;display:block;animation:fadeup 1s ease both}
.hero-kicker{font-family:var(--display);font-size:11px;font-weight:700;letter-spacing:4px;color:var(--muted);animation:fadeup .9s ease both .1s}
.hero h1{font-family:var(--display);font-weight:900;font-size:clamp(40px,8vw,72px);letter-spacing:6px;margin:10px 0 6px;animation:fadeup .9s ease both .2s}
.hero .assoc{font-size:clamp(15px,2.4vw,19px);color:var(--ink2);letter-spacing:2px;text-transform:uppercase;animation:fadeup .9s ease both .3s}
.hero-sub{max-width:560px;margin:18px auto 0;font-size:14px;color:var(--ink2);animation:fadeup .9s ease both .4s}
.tagline-row{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin:28px auto 6px;animation:fadeup .9s ease both .5s}
.tagline{font-family:var(--display);font-size:10px;font-weight:700;letter-spacing:2px;border:1.5px solid var(--ink);border-radius:99px;padding:8px 16px;text-transform:uppercase}
.hero-cta{display:flex;flex-wrap:wrap;justify-content:center;gap:12px;margin-top:30px;animation:fadeup .9s ease both .6s}
@keyframes fadeup{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
/* ============ SECTIONS ============ */
section{padding:64px 0}
.sec-head{text-align:center;margin-bottom:36px}
.sec-head h2{font-family:var(--display);font-weight:900;font-size:clamp(22px,4vw,30px);letter-spacing:3px;text-transform:uppercase}
.sec-head .sub{font-size:12px;color:var(--muted);letter-spacing:2px;text-transform:uppercase;margin-top:8px}
.divider{border:none;border-top:1px solid var(--line);max-width:1040px;margin:0 auto}
/* ============ HOW IT WORKS ============ */
.steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px}
.step{border:1px solid var(--line);border-radius:var(--radius);padding:24px 20px;background:var(--bg)}
.step .n{font-family:var(--display);font-weight:900;font-size:26px;color:var(--muted)}
.step h3{font-family:var(--display);font-size:13px;font-weight:700;letter-spacing:2px;text-transform:uppercase;margin:8px 0 6px}
.step p{font-size:13px;color:var(--ink2)}
/* ============ MEMBERSHIP ============ */
#join{background:var(--bg2)}
.join-wrap{max-width:640px;margin:0 auto}
.note-box{border:1px solid var(--line2);border-radius:14px;padding:14px 18px;font-size:13px;color:var(--ink2);background:var(--bg);margin-bottom:22px}
.note-box b{color:var(--ink)}
.form-card{background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:26px}
.field{margin-bottom:16px}
.field label{display:block;font-family:var(--display);font-size:10px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--ink2);margin-bottom:6px}
.field input{width:100%;border:1px solid var(--line2);border-radius:12px;padding:13px 14px;font-family:var(--font);font-size:14px;background:var(--bg2);color:var(--ink);outline:none;transition:border-color .15s}
.field input:focus{border-color:var(--ink)}
.sig-wrap{border:1.5px dashed var(--line2);border-radius:12px;position:relative;background:var(--bg2)}
.sig-canvas{display:block;width:100%;height:140px;touch-action:none;cursor:crosshair;border-radius:12px}
.sig-hint{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--muted);letter-spacing:1px;pointer-events:none;text-transform:uppercase}
.sig-tools{display:flex;align-items:center;justify-content:space-between;margin-top:8px}
.sig-status{font-size:11px;color:var(--muted);letter-spacing:.5px}
.sig-status.ok{font-weight:700;color:var(--ink)}
.link-btn{background:none;border:none;cursor:pointer;font-family:var(--display);font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--muted);text-decoration:underline}
.link-btn:hover{color:var(--ink)}
.covenant{background:var(--bg2);border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin:18px 0;max-height:190px;overflow-y:auto;font-size:12.5px;color:var(--ink2)}
.covenant h4{font-family:var(--display);font-size:11px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--ink);margin-bottom:8px}
.covenant ol{padding-left:18px;display:grid;gap:6px}
.agree-row{display:flex;gap:10px;align-items:flex-start;margin:16px 0}
.agree-row input{width:18px;height:18px;margin-top:1px;accent-color:var(--black);flex:0 0 18px}
.agree-row label{font-size:12.5px;color:var(--ink2)}
.form-err{font-size:12px;font-weight:700;color:var(--ink);background:var(--bg2);border:1.5px solid var(--ink);border-radius:10px;padding:9px 12px;margin-bottom:12px;display:none}
.form-err.show{display:block}
.form-actions{display:grid;gap:10px}
.member-done{text-align:center;padding:10px 0}
.member-done .check{width:56px;height:56px;border-radius:50%;background:var(--black);color:#fff;font-size:26px;display:flex;align-items:center;justify-content:center;margin:0 auto 14px}
.member-done h3{font-family:var(--display);font-weight:900;font-size:17px;letter-spacing:2px}
.member-code{font-family:var(--display);font-weight:900;font-size:22px;letter-spacing:3px;background:var(--bg2);border:1px solid var(--line);border-radius:12px;padding:12px;margin:16px 0}
.member-meta{font-size:13px;color:var(--ink2);margin-bottom:6px}
/* ============ FOOTER ============ */
footer{border-top:1px solid var(--line);padding:28px 22px 34px;text-align:center}
footer .f-logo{width:52px;height:52px;object-fit:contain;margin:0 auto 10px;display:block}
footer .f-line{font-family:var(--display);font-size:10px;font-weight:700;letter-spacing:3px;text-transform:uppercase;color:var(--muted)}
footer .f-sub{font-size:11px;color:var(--muted);margin-top:6px}
</style>
</head>
<body>
<header>
<div class="header-in">
<img class="logo" src="logo.png" alt="K484">
<div class="brand"><b>KITCHEN 484</b><span>SOLARPUNK SUMMIT</span></div>
<div class="header-right">
<span class="member-chip" id="member-chip" hidden></span>
<a class="btn btn-ghost btn-sm" href="#join" id="nav-join">Join</a>
<a class="btn btn-primary btn-sm" href="index.html">Food Menu</a>
</div>
</div>
</header>
<div class="hero">
<img class="hero-logo" src="logo.png" alt="K484">
<div class="hero-kicker">SOLARPUNK SUMMIT · Oct 8–12</div>
<h1>K484</h1>
<div class="assoc">A Private Membership Association</div>
<p class="hero-sub">Five days. Off-grid kitchen. No seed oils, all organic grass-fed, gluten free and vegetarian friendly. See when the food is on, pre-order, and pay with Bitcoin — no cash, no cards.</p>
<div class="tagline-row">
<span class="tagline">No Seed Oils</span>
<span class="tagline">All Organic Grass Fed</span>
<span class="tagline">Gluten Free &amp; Vegetarian Friendly</span>
<span class="tagline">Off Grid — Zero Waste</span>
</div>
<div class="hero-cta">
<a class="btn btn-primary" href="#join">Become a Member</a>
<a class="btn btn-ghost" href="index.html">Food Menu →</a>
</div>
</div>
<section id="join">
<div class="wrap join-wrap">
<div class="sec-head">
<h2>Membership</h2>
<div class="sub">Kitchen 484 · Private Membership Association</div>
</div>
<div id="join-form-wrap">
<div class="note-box">
<b>Note:</b> This is a private, members-only association. Participation is absolutely voluntary. Membership is granted for the duration of the festival (24 hours per day, Oct 8–12), beginning upon acceptance of this application.
</div>
<form class="form-card" id="join-form" novalidate>
<div class="form-err" id="join-err"></div>
<div class="field">
<label for="m-name">Full Name</label>
<input id="m-name" type="text" placeholder="Your full name" maxlength="80" autocomplete="name">
</div>
<div class="field">
<label for="m-email">Email</label>
<input id="m-email" type="email" placeholder="you@example.com" maxlength="120" autocomplete="email">
</div>
<div class="field">
<label for="m-phone">Phone</label>
<input id="m-phone" type="tel" placeholder="+1 (555) 000-0000" maxlength="30" autocomplete="tel">
</div>
<div class="field">
<label>Digital Signature</label>
<div class="sig-wrap">
<canvas class="sig-canvas" id="sig-canvas"></canvas>
<div class="sig-hint" id="sig-hint">Sign here with mouse or finger</div>
</div>
<div class="sig-tools">
<span class="sig-status" id="sig-status">Signature required to apply</span>
<button type="button" class="link-btn" id="sig-clear">Clear</button>
</div>
</div>
<div class="covenant">
<h4>K484 Membership Covenant</h4>
<p style="margin-bottom:8px">By submitting this application and becoming a member of Kitchen 484, I acknowledge and agree that:</p>
<ol>
<li>I am joining a private, members-only association, not a public business.</li>
<li>Access to kitchen activities, meals, and services is limited to accepted members only.</li>
<li>I agree to abide by the rules, governance, and confidentiality requirements outlined in this Covenant.</li>
<li>Membership is granted for a 24-hour period beginning upon acceptance of this application, unless ended earlier by the member or by Kitchen 484.</li>
<li>I acknowledge that my membership, participation, and personal information are confidential, and that I will respect the privacy of Kitchen 484 and other members.</li>
<li>I acknowledge that Kitchen 484 operates under internal Articles, Bylaws, and policies, which are not publicly distributed, and that I do not acquire governance, voting, or inspection rights by virtue of my membership.</li>
</ol>
</div>
<div class="agree-row">
<input type="checkbox" id="m-agree">
<label for="m-agree">"I have read and agree to the Kitchen 484 Membership Covenant, and I voluntarily apply for membership."</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Sign &amp; Apply for Membership</button>
<a class="btn btn-ghost" href="index.html" style="text-decoration:none">Browse the Food Menu First</a>
</div>
</form>
</div>
<div class="form-card member-done" id="join-done" hidden>
<div class="check">✓</div>
<h3>APPLICATION SUBMITTED</h3>
<div class="member-code" id="done-code">K484-XXXX</div>
<div class="member-meta" id="done-name"></div>
<div class="member-meta" id="done-date"></div>
<p class="member-meta" style="margin-top:10px;font-size:12px;color:var(--muted)">Keep this code — it is your member reference. Your signed covenant is available for download and printing.</p>
<div class="form-actions" style="margin-top:18px">
<button class="btn btn-primary" id="btn-download">Download Signed Agreement</button>
<div style="display:flex;gap:10px">
<a class="btn btn-ghost" href="index.html" style="flex:1;text-decoration:none">Open Food Menu</a>
<button class="btn btn-ghost" id="btn-new-apply" style="flex:1">New Application</button>
</div>
</div>
</div>
</div>
</section>
<div class="divider"></div>
<section id="how">
<div class="wrap">
<div class="sec-head">
<h2>How It Works</h2>
<div class="sub">Five days, three steps</div>
</div>
<div class="steps">
<div class="step">
<div class="n">01</div>
<h3>Check the Schedule</h3>
<p>Every stand posts its serving window for each day. The live timeline shows what's open right now and what's coming.</p>
</div>
<div class="step">
<div class="n">02</div>
<h3>Pre-Order Your Window</h3>
<p>Add items to your order, pick a pickup window, and name your order so we can call it out at the stand.</p>
</div>
<div class="step">
<div class="n">03</div>
<h3>Pay with Bitcoin</h3>
<p>Pay on-chain or via Lightning through BTCPay Server. Get your pickup code the moment your payment confirms.</p>
</div>
</div>
</div>
</section>
<div class="divider"></div>
<footer>
<img class="f-logo" src="logo.png" alt="K484">
<div class="f-line">Off Grid — Zero Waste</div>
<div class="f-sub">KITCHEN 484 · SOLARPUNK SUMMIT · Oct 8–12 · No cash, no cards — Bitcoin only</div>
</footer>
<script>
const $=s=>document.querySelector(s);
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
/* ============================== SIGNATURE PAD ============================== */
const canvas=$('#sig-canvas');
const ctx=canvas.getContext('2d');
let drawing=false, lastPt=null, hasInk=false;
function sizeCanvas(){
const r=canvas.getBoundingClientRect();
const dpr=window.devicePixelRatio||1;
const img=canvas.width?canvas.toDataURL():null;
canvas.width=Math.max(1,r.width*dpr);
canvas.height=140*dpr;
ctx.scale(dpr,dpr);
ctx.lineWidth=2.2;
ctx.lineCap='round';
ctx.lineJoin='round';
ctx.strokeStyle='#0A0A0A';
if(img){const im=new Image();im.onload=()=>ctx.drawImage(im,0,0,r.width,140);im.src=img;}
}
function pt(e){
const r=canvas.getBoundingClientRect();
const t=e.touches?e.touches[0]:e;
return {x:t.clientX-r.left,y:t.clientY-r.top};
}
function startDraw(e){
drawing=true; lastPt=pt(e);
// draw a dot so taps (touch users) register ink, not just drags
ctx.beginPath();
ctx.arc(lastPt.x,lastPt.y,1.2,0,Math.PI*2);
ctx.fillStyle='#0A0A0A';
ctx.fill();
if(!hasInk){
hasInk=true;
$('#sig-hint').style.display='none';
const st=$('#sig-status');
st.textContent='✓ Signed — you can apply now';
st.classList.add('ok');
}
e.preventDefault();
}
function moveDraw(e){
if(!drawing)return;
const p=pt(e);
ctx.beginPath();
ctx.moveTo(lastPt.x,lastPt.y);
ctx.lineTo(p.x,p.y);
ctx.stroke();
lastPt=p;
if(!hasInk){
hasInk=true;
$('#sig-hint').style.display='none';
const st=$('#sig-status');
st.textContent='✓ Signed — you can apply now';
st.classList.add('ok');
}
e.preventDefault();
}
function endDraw(){drawing=false;lastPt=null;}
canvas.addEventListener('mousedown',startDraw);
canvas.addEventListener('mousemove',moveDraw);
window.addEventListener('mouseup',endDraw);
canvas.addEventListener('touchstart',startDraw,{passive:false});
canvas.addEventListener('touchmove',moveDraw,{passive:false});
canvas.addEventListener('touchend',endDraw);
$('#sig-clear').onclick=()=>{
ctx.clearRect(0,0,canvas.width,canvas.height);
hasInk=false;
$('#sig-hint').style.display='flex';
const st=$('#sig-status');
st.textContent='Signature required to apply';
st.classList.remove('ok');
};
window.addEventListener('resize',sizeCanvas);
sizeCanvas();
/* ============================== MEMBERSHIP ============================== */
const LS_MEMBER='slk_member';
const LS_MEMBERS='slk_members';
function loadMember(){try{return JSON.parse(localStorage.getItem(LS_MEMBER)||'null');}catch(e){return null;}}
function saveMember(m){
localStorage.setItem(LS_MEMBER,JSON.stringify(m));
const all=JSON.parse(localStorage.getItem(LS_MEMBERS)||'[]');
all.push(m);
localStorage.setItem(LS_MEMBERS,JSON.stringify(all));
}
function showMember(m){
$('#join-form-wrap').hidden=true;
$('#join-done').hidden=false;
$('#done-code').textContent=m.code;
$('#done-name').textContent=m.fullName;
$('#done-date').textContent='Applied '+new Date(m.ts).toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'});
const chip=$('#member-chip');
chip.hidden=false;
chip.textContent=m.code;
document.getElementById('nav-join').textContent='Member';
$('#join').scrollIntoView({behavior:'smooth'});
}
const existing=loadMember();
if(existing)showMember(existing);
$('#join-form').addEventListener('submit',e=>{
e.preventDefault();
const err=$('#join-err');
err.classList.remove('show');
const name=$('#m-name').value.trim();
const email=$('#m-email').value.trim();
const phone=$('#m-phone').value.trim();
const agree=$('#m-agree').checked;
let problem=null;
if(!name)problem='Please enter your full name.';
else if(!email||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))problem='Please enter a valid email address.';
else if(!phone)problem='Please enter a phone number.';
else if(!hasInk)problem='Please sign in the signature box.';
else if(!agree)problem='Please check the covenant agreement box.';
if(problem){
err.textContent=problem;
err.classList.add('show');
err.scrollIntoView({behavior:'smooth',block:'center'});
return;
}
const member={
code:'K484-'+Math.random().toString(36).slice(2,6).toUpperCase(),
fullName:name, email, phone,
ts:Date.now(),
signature:canvas.toDataURL('image/png')
};
saveMember(member);
showMember(member);
});
$('#btn-new-apply').onclick=()=>{
localStorage.removeItem(LS_MEMBER);
location.reload();
};
/* ============================== AGREEMENT DOWNLOAD ============================== */
$('#btn-download').onclick=()=>{
const m=loadMember();
if(!m){toast('No application found');return;}
const d=new Date(m.ts);
const html='<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Kitchen 484 Membership Agreement - '+esc(m.fullName)+'</title>'+
'<style>'+
'body{font-family:Georgia,serif;color:#000;max-width:720px;margin:40px auto;padding:0 24px;line-height:1.6;font-size:14px}'+
'h1{font-size:22px;letter-spacing:2px;text-transform:uppercase;text-align:center}'+
'h2{font-size:14px;letter-spacing:1px;text-transform:uppercase;text-align:center;font-weight:normal;color:#333;margin-top:4px}'+
'.box{border:1px solid #000;padding:18px 22px;margin:22px 0}'+
'.info p{margin:6px 0}'+
'.info b{display:inline-block;width:110px}'+
'ol{padding-left:20px}li{margin:8px 0}'+
'.sig{margin-top:30px;text-align:center}'+
'.sig img{height:90px;border:1px solid #999;border-radius:6px;padding:6px;background:#fff}'+
'.sig .line{margin-top:6px;font-size:12px;color:#333}'+
'.declare{margin-top:24px;font-style:italic}'+
'@media print{body{margin:20px auto}}'+
'</style></head><body>'+
'<h1>Kitchen 484</h1><h2>Membership Agreement — '+esc(m.code)+'</h2>'+
'<div class="box info">'+
'<p><b>Member:</b> '+esc(m.fullName)+'</p>'+
'<p><b>Email:</b> '+esc(m.email)+'</p>'+
'<p><b>Phone:</b> '+esc(m.phone)+'</p>'+
'<p><b>Applied:</b> '+d.toLocaleString('en-US')+'</p>'+
'</div>'+
'<p>Note: This is a private, members-only association. Participation is absolutely voluntary.</p>'+
'<h2 style="text-align:left;margin-top:20px">K484 Membership Covenant</h2>'+
'<p>By submitting this application and becoming a member of Kitchen 484, I acknowledge and agree that:</p>'+
'<ol>'+
'<li>I am joining a private, members-only association, not a public business.</li>'+
'<li>Access to kitchen activities, meals, and services is limited to accepted members only.</li>'+
'<li>I agree to abide by the rules, governance, and confidentiality requirements outlined in this Covenant.</li>'+
'<li>Membership is granted for a 24-hour period beginning upon acceptance of this application, unless ended earlier by the member or by Kitchen 484.</li>'+
'<li>I acknowledge that my membership, participation, and personal information are confidential, and that I will respect the privacy of Kitchen 484 and other members.</li>'+
'<li>I acknowledge that Kitchen 484 operates under internal Articles, Bylaws, and policies, which are not publicly distributed, and that I do not acquire governance, voting, or inspection rights by virtue of my membership.</li>'+
'</ol>'+
'<p class="declare">"I have read and agree to the Kitchen 484 Membership Covenant, and I voluntarily apply for membership."</p>'+
'<div class="sig"><img src="'+m.signature+'" alt="signature"><div class="line">Digital signature — '+esc(m.fullName)+' · '+d.toLocaleDateString('en-US')+'</div></div>'+
'<script>setTimeout(function(){window.print()},600)</'+'script>'+
'</body></html>';
const w=window.open('','_blank');
if(!w){toast('Pop-up blocked — allow pop-ups to download');return;}
w.document.write(html);
w.document.close();
};
function toast(msg){
let t=document.getElementById('h-toast');
if(!t){t=document.createElement('div');t.id='h-toast';t.style.cssText='position:fixed;left:50%;bottom:36px;transform:translateX(-50%) translateY(20px);background:#0A0A0A;color:#fff;font-size:12px;font-weight:700;padding:10px 18px;border-radius:99px;opacity:0;transition:all .25s;z-index:200;pointer-events:none';document.body.appendChild(t);}
t.textContent=msg;t.style.opacity='1';t.style.transform='translateX(-50%) translateY(0)';
clearTimeout(t._x);t._x=setTimeout(()=>{t.style.opacity='0';t.style.transform='translateX(-50%) translateY(20px)';},2200);
}
</script>
</body>
</html>