SolLunar-Kitchen/_shot_cosmic.html

972 lines
62 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>KITCHEN 484 · SOLARPUNK SUMMIT</title>
<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">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400;1,600&family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600&display=swap" rel="stylesheet">
<script>/* Start in the white theme. */try{localStorage.removeItem('kitchen484-theme');}catch(e){}</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
:root{
--bg:#FFFFFF; --bg2:#f8fafc;
--ink:#0A0A0A; --ink2:#475569; --muted:#94a3b8;
--line:#e2e8f0; --line2:#cbd5e1;
--blue:#3b82f6; --blue-soft:#eff6ff;
--pink:#d946ef; --pink-soft:#fdf4ff;
--gold:#eab308; --gold-soft:#fefce8;
--green:#22c55e; --green-soft:#f0fdf4;
--red:#ef4444; --red-soft:#fef2f2;
--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);
}
/* ============ COSMIC THEME — Stardust (kaymade/cosmic) · Deep Space · Deep sidebar ============ */
:root[data-theme="cosmic"]{
--bg:#171109; --bg2:#0d0a06;
--ink:#fff5dc; --ink2:#d5bd8d; --muted:#917b58;
--line:#3a2d19; --line2:#54411f;
--blue:#87E6FB; --blue-soft:#10313b; /* Light Blue */
--pink:#F3B407; --pink-soft:#33270d; /* Gold (was magenta) */
--gold:#F3B407; --gold-soft:#33270d; /* Gold */
--green:#87E6FB; --green-soft:#10313b;
--red:#F3B407; --red-soft:#33270d;
--black:#100b05;
--logo-frame:rgba(255,245,220,.10);
--radius:18px;
--shadow:0 1px 2px rgb(0 0 0 / .4), 0 14px 34px -14px rgb(0 0 0 / .8);
--font:'Source Serif 4',Georgia,'Times New Roman',serif;
--display:'Cormorant Garamond','Orbitron',Georgia,serif;
}
*{box-sizing:border-box;margin:0;padding:0}
html{line-height:1.5}
body{font-family:var(--font);background:var(--bg2);color:var(--ink);-webkit-font-smoothing:antialiased}
#app{max-width:520px;margin:0 auto;min-height:100dvh;background:var(--bg);display:flex;flex-direction:column;position:relative;box-shadow:0 0 40px rgb(0 0 0/.06)}
/* ---------- header ---------- */
header{position:sticky;top:0;z-index:40;background:rgb(255 255 255/.85);backdrop-filter:blur(12px);border-bottom:1px solid var(--line);padding:12px 16px;display:flex;align-items:center;gap:10px}
.logo{width:44px;height:44px;flex:0 0 44px;object-fit:contain}
.brand{display:flex;flex-direction:column;line-height:1.15}
.brand b{font-family:var(--display);font-weight:900;font-size:15px;letter-spacing:2px}
.brand span{font-size:10px;color:var(--muted);letter-spacing:1.5px}
.header-right{margin-left:auto;display:flex;align-items:center;gap:8px}
.clock{font-family:var(--display);font-size:12px;font-weight:700;color:var(--ink2);letter-spacing:1px;background:var(--bg2);border:1px solid var(--line);border-radius:999px;padding:4px 10px}
.icon-btn{width:36px;height:36px;border-radius:999px;border:1px solid var(--line);background:#fff;color:var(--ink2);display:grid;place-items:center;cursor:pointer;transition:all .15s}
.icon-btn:active{transform:scale(.92)}
.icon-btn svg{width:17px;height:17px}
/* ---------- hero ---------- */
.hero{padding:26px 18px 18px;background:linear-gradient(180deg,#fff 0%,var(--bg2) 100%)}
.hero h1{font-family:var(--display);font-weight:900;font-size:26px;letter-spacing:3px;margin:10px 0 4px}
.hero h1 em{font-style:normal;background:linear-gradient(90deg,var(--blue),var(--pink));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.hero p{color:var(--ink2);font-size:13px;max-width:360px}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 16px}
.chip{font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;padding:5px 10px;border-radius:999px;border:1px solid}
.chip.c1{color:var(--ink);border-color:var(--ink);background:var(--black);color:#fff}
.chip.c2{color:#166534;border-color:#bbf7d0;background:var(--green-soft)}
.chip.c3{color:#86198f;border-color:#f5d0fe;background:var(--pink-soft)}
.chip.c4{color:#854d0e;border-color:#fde68a;background:var(--gold-soft)}
.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:14px;padding:13px 22px;transition:transform .12s,box-shadow .12s}
.btn:active{transform:scale(.97)}
.btn-primary{background:var(--black);color:#fff;width:100%;box-shadow:var(--shadow)}
.btn-primary:disabled{opacity:.35;cursor:not-allowed}
.btn-ghost{background:#fff;color:var(--ink);border:1px solid var(--line2)}
.btn-blue{background:var(--blue);color:#fff;width:100%}
.btn-pink{background:linear-gradient(90deg,var(--blue),var(--pink));color:#fff;width:100%}
.btn-sm{padding:9px 14px;font-size:12px;border-radius:11px;width:auto}
/* ---------- day tabs ---------- */
.daytabs{display:flex;gap:8px;padding:4px 16px 12px;position:sticky;top:69px;background:rgb(255 255 255/.9);backdrop-filter:blur(8px);z-index:30}
.daytab{flex:1;border:1px solid var(--line);background:#fff;border-radius:14px;padding:9px 6px;cursor:pointer;text-align:center;transition:all .15s}
.daytab .d1{font-family:var(--display);font-weight:700;font-size:11px;letter-spacing:1px}
.daytab .d2{font-size:10px;color:var(--muted)}
.daytab.active{background:var(--black);border-color:var(--black);color:#fff;box-shadow:var(--shadow)}
.daytab.active .d2{color:#a3a3a3}
.daytab.today::after{content:'TODAY';display:block;font-size:8px;font-weight:900;letter-spacing:1px;color:var(--pink);margin-top:2px}
.daytab.active.today::after{color:var(--pink)}
/* ---------- timeline ---------- */
.timeline{padding:8px 16px 24px;position:relative}
.tl-head{display:flex;justify-content:space-between;align-items:baseline;margin:6px 2px 10px}
.tl-head h2{font-size:14px;font-weight:800;letter-spacing:.5px;text-transform:uppercase}
.tl-head span{font-size:11px;color:var(--muted)}
.tl-wrap{position:relative;padding-left:26px}
.tl-rail{position:absolute;left:9px;top:6px;bottom:6px;width:2px;background:var(--line);border-radius:2px}
.nowline{position:absolute;left:-4px;right:-8px;height:2px;background:var(--red);border-radius:2px;z-index:5}
.nowline::before{content:'NOW';position:absolute;left:-2px;top:-16px;font-size:8px;font-weight:900;letter-spacing:1px;color:var(--red);background:#fff;padding:1px 4px;border-radius:4px}
.nowline::after{content:'';position:absolute;left:-5px;top:-2px;width:8px;height:8px;border-radius:99px;background:var(--red);animation:pulse 1.6s infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(1.5)}}
.item-card{position:relative;background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:14px;margin-bottom:12px;box-shadow:0 1px 2px rgb(10 10 10/.04);transition:border-color .2s,box-shadow .2s}
.item-card.is-open{border-color:var(--green);box-shadow:0 0 0 3px var(--green-soft),var(--shadow)}
.item-card.is-soon{border-color:#bfdbfe}
.item-card.is-ended{opacity:.62}
.tl-dot{position:absolute;left:-21px;top:22px;width:12px;height:12px;border-radius:99px;background:#fff;border:3px solid var(--line2);z-index:6}
.item-card.is-open .tl-dot{border-color:var(--green);background:var(--green)}
.item-card.is-soon .tl-dot{border-color:var(--blue)}
.tl-time{position:absolute;left:-26px;top:18px;transform:translateX(-100%);width:52px;text-align:right;font-family:var(--display);font-size:9px;font-weight:700;color:var(--muted);letter-spacing:.5px;line-height:1.3}
.item-top{display:flex;gap:12px;align-items:flex-start}
.item-emoji{width:46px;height:46px;flex:0 0 46px;border-radius:14px;background:var(--bg2);border:1px solid var(--line);display:grid;place-items:center;font-size:24px}
.item-name{font-size:15px;font-weight:800;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.item-desc{font-size:12px;color:var(--ink2);margin-top:2px}
.item-price{margin-left:auto;font-family:var(--display);font-weight:900;font-size:16px;white-space:nowrap}
.item-foot{display:flex;align-items:center;gap:8px;margin-top:12px;flex-wrap:wrap}
.status-pill{font-size:10px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;padding:5px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:6px}
.status-pill .dot{width:6px;height:6px;border-radius:99px;background:currentColor}
.sp-open{background:var(--green-soft);color:#15803d}
.sp-open .dot{animation:pulse 1.4s infinite}
.sp-soon{background:var(--blue-soft);color:#1d4ed8}
.sp-upcoming{background:var(--bg2);color:var(--muted);border:1px solid var(--line)}
.sp-ended{background:var(--bg2);color:var(--muted)}
.tags{display:flex;gap:4px}
.tag{font-size:9px;font-weight:800;letter-spacing:.5px;padding:3px 7px;border-radius:7px;background:var(--bg2);border:1px solid var(--line);color:var(--ink2)}
.add-btn{margin-left:auto;border:1px solid var(--black);background:#fff;color:var(--ink);font-weight:800;font-size:12px;border-radius:11px;padding:8px 14px;cursor:pointer;transition:all .12s;white-space:nowrap}
.add-btn:active{transform:scale(.94);background:var(--black);color:#fff}
.add-btn.in-cart{background:var(--green-soft);border-color:#bbf7d0;color:#15803d}
/* ---------- orders ---------- */
.orders{padding:16px}
.orders h2{font-size:14px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;margin-bottom:4px}
.orders .sub{font-size:12px;color:var(--muted);margin-bottom:14px}
.empty{text-align:center;padding:56px 20px;color:var(--muted)}
.empty .big{font-size:40px;margin-bottom:10px}
.order-card{background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:14px;margin-bottom:12px;box-shadow:0 1px 2px rgb(10 10 10/.04)}
.order-top{display:flex;align-items:center;gap:10px}
.order-code{font-family:var(--display);font-weight:900;font-size:15px;letter-spacing:1px}
.badge{font-size:9px;font-weight:900;letter-spacing:1px;padding:4px 9px;border-radius:99px;text-transform:uppercase}
.b-paid{background:var(--green-soft);color:#15803d}
.b-pending{background:var(--gold-soft);color:#854d0e}
.b-failed{background:var(--red-soft);color:#b91c1c}
.b-demo{background:var(--pink-soft);color:#86198f}
.order-meta{font-size:11px;color:var(--muted);margin:6px 0}
.order-items{font-size:12px;color:var(--ink2);border-top:1px dashed var(--line);padding-top:8px;margin-top:8px}
.order-total{display:flex;justify-content:space-between;font-size:13px;font-weight:800;margin-top:6px}
.order-total .btc{font-family:var(--display);font-size:11px;color:var(--blue);font-weight:700}
.order-actions{display:flex;gap:8px;margin-top:10px}
/* ---------- bottom nav + fab ---------- */
nav.bottom{position:sticky;bottom:0;z-index:40;background:rgb(255 255 255/.92);backdrop-filter:blur(12px);border-top:1px solid var(--line);display:flex;padding:8px 12px calc(10px + env(safe-area-inset-bottom))}
.nav-btn{flex:1;border:none;background:none;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:3px;padding:7px;border-radius:12px;font-size:10px;font-weight:700;letter-spacing:.5px;color:var(--muted);transition:all .15s}
.nav-btn svg{width:20px;height:20px}
.nav-btn.active{color:var(--ink);background:var(--bg2)}
.fab{position:absolute;right:16px;bottom:calc(74px + env(safe-area-inset-bottom));z-index:45;width:58px;height:58px;border-radius:20px;border:none;cursor:pointer;background:var(--black);color:#fff;display:grid;place-items:center;box-shadow:0 10px 24px -8px rgb(10 10 10/.5);transition:transform .15s}
.fab:active{transform:scale(.92)}
.fab svg{width:24px;height:24px}
.fab .count{position:absolute;top:-7px;right:-7px;min-width:22px;height:22px;border-radius:99px;background:var(--pink);color:#fff;font-size:11px;font-weight:900;display:grid;place-items:center;padding:0 5px;box-shadow:0 2px 6px rgb(0 0 0/.3)}
.fab.hidden{display:none}
/* ---------- sheet / modal ---------- */
.overlay{position:fixed;inset:0;background:rgb(10 10 10/.45);backdrop-filter:blur(3px);z-index:100;display:none;align-items:flex-end;justify-content:center}
.overlay.show{display:flex}
.sheet{width:100%;max-width:520px;background:#fff;border-radius:24px 24px 0 0;padding:10px 18px calc(22px + env(safe-area-inset-bottom));max-height:88dvh;overflow-y:auto;animation:slideUp .3s cubic-bezier(.16,1,.3,1)}
@keyframes slideUp{from{transform:translateY(60px);opacity:0}to{transform:translateY(0);opacity:1}}
.grab{width:40px;height:4px;border-radius:4px;background:var(--line2);margin:4px auto 14px}
.sheet h3{font-family:var(--display);font-weight:900;font-size:16px;letter-spacing:1.5px;margin-bottom:4px}
.sheet .sub{font-size:12px;color:var(--muted);margin-bottom:14px}
.cart-row{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--line)}
.cart-row .em{width:40px;height:40px;border-radius:12px;background:var(--bg2);display:grid;place-items:center;font-size:20px;border:1px solid var(--line)}
.cart-row .nm{font-size:13px;font-weight:700}
.cart-row .pt{font-size:11px;color:var(--muted)}
.qty{display:flex;align-items:center;gap:10px;margin-left:auto}
.qty button{width:28px;height:28px;border-radius:9px;border:1px solid var(--line2);background:#fff;font-size:15px;font-weight:800;cursor:pointer;color:var(--ink)}
.qty button:active{transform:scale(.9)}
.qty span{font-family:var(--display);font-weight:700;font-size:13px;min-width:18px;text-align:center}
.cart-row .lp{font-family:var(--display);font-weight:900;font-size:13px;width:52px;text-align:right}
.total-row{display:flex;justify-content:space-between;align-items:baseline;padding:14px 0 4px}
.total-row .t{font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:1px}
.total-row .v{font-family:var(--display);font-weight:900;font-size:20px}
.btc-est{font-family:var(--display);font-size:11px;color:var(--blue);font-weight:700;margin-bottom:14px;text-align:right}
.btc-est small{color:var(--muted);font-family:var(--font);font-weight:400}
.field{margin-bottom:12px}
.field label{display:block;font-size:11px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:var(--ink2);margin-bottom:6px}
.field input,.field select,.field textarea{width:100%;border:1px solid var(--line2);border-radius:12px;padding:12px 14px;font-family:var(--font);font-size:14px;background:var(--bg2);color:var(--ink);outline:none;transition:border-color .15s}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--blue)}
.field textarea{resize:none;height:70px}
.hint{font-size:11px;color:var(--muted);margin:-6px 0 12px}
.pay-box{border:1px solid var(--line);border-radius:var(--radius);background:var(--bg2);padding:16px;text-align:center;margin-bottom:14px}
.pay-amount{font-family:var(--display);font-weight:900;font-size:28px;margin:6px 0 2px}
.pay-btc{font-family:var(--display);font-weight:700;font-size:13px;color:var(--blue);margin-bottom:12px}
.qr-wrap{background:#fff;border:1px solid var(--line);border-radius:16px;padding:12px;display:inline-block}
.qr-wrap canvas,.qr-wrap img{display:block;border-radius:8px}
.addr-row{display:flex;align-items:center;gap:8px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-top:12px}
.addr-row code{flex:1;font-size:10px;word-break:break-all;color:var(--ink2);font-family:ui-monospace,Menlo,monospace}
.copy-btn{border:1px solid var(--line2);background:var(--bg2);border-radius:9px;font-size:11px;font-weight:800;padding:7px 12px;cursor:pointer;white-space:nowrap}
.pay-status{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;font-weight:700;color:var(--ink2);margin:12px 0}
.spinner{width:14px;height:14px;border:2px solid var(--line);border-top-color:var(--blue);border-radius:99px;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.pay-status.ok{color:#15803d}
.pay-status.err{color:#b91c1c}
.pay-tabs{display:flex;gap:6px;margin-bottom:12px}
.pay-tab{flex:1;border:1px solid var(--line);background:#fff;border-radius:11px;padding:9px;font-size:12px;font-weight:800;cursor:pointer;color:var(--muted)}
.pay-tab.active{background:var(--black);color:#fff;border-color:var(--black)}
.demo-note{background:var(--gold-soft);border:1px solid #fde68a;color:#854d0e;font-size:11px;font-weight:600;border-radius:12px;padding:10px 12px;margin-bottom:12px}
.success{text-align:center;padding:18px 0 6px}
.success .check{width:74px;height:74px;border-radius:99px;background:var(--green-soft);border:2px solid #bbf7d0;display:grid;place-items:center;margin:0 auto 14px;font-size:34px;animation:pop .5s cubic-bezier(.16,1,.3,1)}
@keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}
.success h3{margin-bottom:6px}
.success .code{font-family:var(--display);font-weight:900;font-size:24px;letter-spacing:3px;background:var(--bg2);border:1px dashed var(--line2);border-radius:14px;padding:12px;margin:14px 0}
.success .pick{font-size:13px;color:var(--ink2);margin-bottom:16px}
.success .pick b{color:var(--ink)}
/* ---------- settings ---------- */
.modal{position:fixed;inset:0;background:rgb(10 10 10/.45);backdrop-filter:blur(3px);z-index:110;display:none;align-items:center;justify-content:center;padding:20px}
.modal.show{display:flex}
.modal-box{width:100%;max-width:460px;background:#fff;border-radius:22px;padding:20px;max-height:86dvh;overflow-y:auto;animation:slideUp .3s cubic-bezier(.16,1,.3,1)}
.modal-box h3{font-family:var(--display);font-weight:900;font-size:15px;letter-spacing:1.5px;margin-bottom:10px}
.test-result{font-size:11px;font-weight:700;border-radius:10px;padding:9px 12px;margin-bottom:12px;display:none}
.test-result.ok{display:block;background:var(--green-soft);color:#15803d}
.test-result.err{display:block;background:var(--red-soft);color:#b91c1c}
footer{padding:18px;text-align:center;font-size:10px;color:var(--muted);letter-spacing:1px;text-transform:uppercase;border-top:1px solid var(--line);margin-top:auto}
.toast{position:fixed;left:50%;bottom:100px;transform:translateX(-50%) translateY(20px);background:var(--black);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;white-space:nowrap}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
/* ============ COSMIC THEME — component overrides (Deep Space atmosphere) ============
White theme stays byte-for-byte; only :root[data-theme="cosmic"] re-skins. */
:root[data-theme="cosmic"] body{
background:
radial-gradient(circle at 8% 13%, rgba(255,245,220,.80) 0 1px, transparent 1.7px),
radial-gradient(circle at 27% 7%, rgba(135,230,251,.85) 0 1px, transparent 1.8px),
radial-gradient(circle at 73% 19%, rgba(255,245,220,.75) 0 1.2px, transparent 1.9px),
radial-gradient(circle at 91% 42%, rgba(243,180,7,.85) 0 1px, transparent 1.8px),
radial-gradient(circle at 61% 78%, rgba(255,245,220,.70) 0 1px, transparent 1.7px),
radial-gradient(circle at 18% 84%, rgba(243,180,7,.85) 0 1.2px, transparent 2px),
radial-gradient(circle at 12% 4%, rgba(243,180,7,.30), transparent 55rem),
radial-gradient(circle at 88% 12%, rgba(135,230,251,.24), transparent 44rem),
radial-gradient(circle at 52% 100%,rgba(243,180,7,.22), transparent 48rem),
radial-gradient(ellipse at center, transparent 34%, rgba(13,10,6,.55) 100%),
linear-gradient(145deg, var(--bg), var(--bg2));
background-blend-mode:screen,screen,screen,screen,screen,screen,screen,screen,screen,multiply,normal;
background-attachment:fixed;
}
:root[data-theme="cosmic"] #app{box-shadow:0 0 60px rgb(0 0 0/.6)}
:root[data-theme="cosmic"] header{background:rgba(13,10,6,.72)}
:root[data-theme="cosmic"] .hero{background:linear-gradient(180deg,rgba(23,17,9,.9) 0%,var(--bg) 100%)}
:root[data-theme="cosmic"] .icon-btn{background:rgba(255,245,220,.06);border-color:var(--line)}
:root[data-theme="cosmic"] .clock{background:rgba(255,245,220,.05)}
:root[data-theme="cosmic"] .chip.c1{background:var(--blue-soft);border-color:#1c4a56;color:#cdeffb}
:root[data-theme="cosmic"] .chip.c2{background:var(--green-soft);border-color:#1d4a44;color:#a7e6dd}
:root[data-theme="cosmic"] .chip.c3{background:var(--pink-soft);border-color:#4a3810;color:#f6d488}
:root[data-theme="cosmic"] .chip.c4{background:var(--gold-soft);border-color:#4a3810;color:#f6d488}
:root[data-theme="cosmic"] .btn-ghost{background:rgba(255,245,220,.05);color:var(--ink)}
:root[data-theme="cosmic"] .btn-blue{background:var(--blue);color:#0a0f14}
:root[data-theme="cosmic"] .btn-pink{background:linear-gradient(90deg,var(--blue),var(--gold));color:#0a0f14}
:root[data-theme="cosmic"] .btn-primary{background:var(--gold);color:#171109}
:root[data-theme="cosmic"] .daytabs{background:rgba(13,10,6,.7)}
:root[data-theme="cosmic"] .daytab{background:rgba(255,245,220,.045)}
:root[data-theme="cosmic"] .daytab.active{background:linear-gradient(135deg,#2a1e0e,#1c1309);border-color:var(--gold);color:var(--ink)}
:root[data-theme="cosmic"] .daytab.today::after,
:root[data-theme="cosmic"] .daytab.active.today::after{color:var(--gold)}
:root[data-theme="cosmic"] .nowline::before{background:#171109}
:root[data-theme="cosmic"] .item-card{background:linear-gradient(160deg,rgba(33,24,12,.92),rgba(20,15,8,.92))}
:root[data-theme="cosmic"] .item-card.is-open{box-shadow:0 0 0 3px rgba(135,230,251,.16),0 0 26px -6px rgba(135,230,251,.35),var(--shadow)}
:root[data-theme="cosmic"] .item-card.is-soon{border-color:#1c4a56}
:root[data-theme="cosmic"] .tl-dot{background:var(--bg)}
:root[data-theme="cosmic"] .item-emoji{background:rgba(255,245,220,.05)}
:root[data-theme="cosmic"] .tag{background:rgba(255,245,220,.05)}
:root[data-theme="cosmic"] .add-btn{background:transparent;border-color:var(--line2);color:var(--ink)}
:root[data-theme="cosmic"] .add-btn:active{background:var(--gold);color:#171109}
:root[data-theme="cosmic"] .add-btn.in-cart{background:var(--green-soft);border-color:#1d4a44;color:#a7e6dd}
:root[data-theme="cosmic"] .order-card{background:linear-gradient(160deg,rgba(33,24,12,.92),rgba(20,15,8,.92))}
:root[data-theme="cosmic"] .sp-upcoming,
:root[data-theme="cosmic"] .sp-ended{background:rgba(255,245,220,.05)}
:root[data-theme="cosmic"] .b-pending{background:var(--gold-soft);color:#f6d488}
:root[data-theme="cosmic"] .b-demo{background:var(--pink-soft);color:#f6d488}
:root[data-theme="cosmic"] .b-failed{background:var(--red-soft);color:#f5b3a9}
:root[data-theme="cosmic"] nav.bottom{background:rgba(13,10,6,.8)}
:root[data-theme="cosmic"] .nav-btn.active{background:rgba(255,245,220,.07)}
:root[data-theme="cosmic"] .fab{background:var(--gold);color:#171109}
:root[data-theme="cosmic"] .fab .count{background:var(--blue);color:#0a0f14}
:root[data-theme="cosmic"] .sheet{background:linear-gradient(180deg,#1c140b,#140f08)}
:root[data-theme="cosmic"] .sheet h3,
:root[data-theme="cosmic"] .modal-box h3{color:var(--gold);font-weight:700;letter-spacing:.5px}
:root[data-theme="cosmic"] .cart-row .em{background:rgba(255,245,220,.05)}
:root[data-theme="cosmic"] .qty button{background:rgba(255,245,220,.05);border-color:var(--line2)}
:root[data-theme="cosmic"] .field input,
:root[data-theme="cosmic"] .field select,
:root[data-theme="cosmic"] .field textarea{background:rgba(13,10,6,.6);border-color:var(--line2)}
:root[data-theme="cosmic"] .pay-box{background:rgba(13,10,6,.55)}
:root[data-theme="cosmic"] .qr-wrap{background:#fff}
:root[data-theme="cosmic"] .addr-row{background:rgba(255,245,220,.05)}
:root[data-theme="cosmic"] .copy-btn{background:rgba(255,245,220,.06)}
:root[data-theme="cosmic"] .demo-note{background:var(--gold-soft);border-color:#4a3810;color:#f6d488}
:root[data-theme="cosmic"] .success .check{background:var(--green-soft);border-color:#1d4a44}
:root[data-theme="cosmic"] .success .code{background:rgba(255,245,220,.05)}
:root[data-theme="cosmic"] .modal-box{background:linear-gradient(180deg,#1c140b,#140f08)}
:root[data-theme="cosmic"] .pay-tab{background:rgba(255,245,220,.045)}
:root[data-theme="cosmic"] .pay-tab.active{background:var(--gold);color:#171109;border-color:var(--gold)}
:root[data-theme="cosmic"] .toast{background:var(--ink);color:#171109}
/* Keep the logo pure black & white in both themes (it is a white tile w/ black mark).
Give it a hairline frame only in cosmic so the white tile reads on the starfield. */
:root[data-theme="cosmic"] .logo{border-radius:12px;box-shadow:0 0 0 1px var(--logo-frame),0 6px 20px -8px rgba(0,0,0,.7)}
:root[data-theme="cosmic"] .hero h1{color:var(--ink)}
:root[data-theme="cosmic"] .hero h1 em{background:linear-gradient(90deg,var(--blue),var(--gold))}
:root[data-theme="cosmic"] footer{border-top-color:var(--line);background:rgba(13,10,6,.5)}
/* theme toggle button */
.theme-toggle{font-family:var(--display);font-size:11px;font-weight:800;letter-spacing:1px;border-radius:999px;border:1px solid var(--line);background:rgba(255,255,255,.05);color:var(--ink2);padding:6px 12px;cursor:pointer;transition:all .15s;white-space:nowrap}
.theme-toggle:active{transform:scale(.95)}
</style>
</head>
<body>
<div id="app">
<header>
<img class="logo" src="logo.png" alt="KITCHEN 484">
<div class="brand"><b>KITCHEN 484</b><span>SOLARPUNK SUMMIT</span></div>
<div class="header-right">
<a class="icon-btn" href="home.html" aria-label="Home"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h5v-6h4v6h5V10"/></svg></a>
<div class="clock" id="clock">--:--</div>
<button class="theme-toggle" id="btn-theme" aria-label="Toggle theme" title="Switch theme">✦ COSMIC</button>
<button class="icon-btn" id="btn-settings" aria-label="Settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
</div>
</header>
<main style="flex:1;display:flex;flex-direction:column">
<!-- SCHEDULE VIEW -->
<section id="view-schedule">
<div class="hero">
<div class="chips">
<span class="chip c1">NO SEED OILS</span>
<span class="chip c2">GRASS FED</span>
<span class="chip c3">VEG FRIENDLY</span>
<span class="chip c4">CASHLESS · BTC</span>
</div>
<h1>WHEN'S <em>FOOD</em> ON?</h1>
<p>Live schedule for the Solarpunk Summit kitchen. See when each stand is serving, pre-order for a window, and pay with Bitcoin — no cash, no cards.</p>
</div>
<div class="daytabs" id="daytabs"></div>
<div class="timeline" id="timeline">
<div class="tl-head"><h2>Serving schedule</h2><span id="tl-sub"></span></div>
<div class="tl-wrap" id="tl-wrap">
<div class="tl-rail"></div>
</div>
</div>
</section>
<!-- ORDERS VIEW -->
<section id="view-orders" hidden>
<div class="orders">
<h2>My orders</h2>
<div class="sub">Show the code at the pickup stand. Paid orders are confirmed on-chain.</div>
<div id="orders-list"></div>
</div>
</section>
<footer>KITCHEN 484 · Off-grid · Zero waste · Powered by BTCPay</footer>
</main>
<nav class="bottom">
<button class="nav-btn active" data-view="schedule">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>
SCHEDULE
</button>
<button class="nav-btn" data-view="orders">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 12h6M9 16h4"/></svg>
ORDERS
</button>
</nav>
<button class="fab hidden" id="fab" aria-label="Cart">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>
<span class="count" id="fab-count">0</span>
</button>
</div>
<!-- SHEET -->
<div class="overlay" id="sheet-overlay"><div class="sheet" id="sheet"></div></div>
<!-- SETTINGS MODAL -->
<div class="modal" id="settings-modal">
<div class="modal-box">
<h3>⚙️ PAYMENT SETTINGS</h3>
<p style="font-size:12px;color:var(--ink2);margin-bottom:14px">Connect a <b>BTCPay Server</b> store to receive real Bitcoin payments. Find your Store ID in the store's settings on your BTCPay instance, and generate a store API key with "Create invoice" permission.</p>
<div class="field"><label>BTCPay server URL</label><input id="set-url" type="url" placeholder="https://btcpay.yourhost.com"></div>
<div class="field"><label>Store ID</label><input id="set-store" type="text" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></div>
<div class="field"><label>Store API key (optional but recommended)</label><input id="set-key" type="password" placeholder="lk_… / bk_…"></div>
<div class="test-result" id="test-result"></div>
<div style="display:flex;gap:8px;margin-bottom:10px">
<button class="btn btn-ghost btn-sm" id="btn-test">Test connection</button>
<button class="btn btn-primary btn-sm" id="btn-save-settings" style="margin-left:auto">Save</button>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-ghost btn-sm" id="btn-clear-orders">Clear orders</button>
<button class="btn btn-ghost btn-sm" style="margin-left:auto" id="btn-close-settings">Close</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
"use strict";
window.__errs=[];
window.addEventListener('error',e=>{
window.__errs.push(String(e.message)+' @'+(e.lineno||'?'));
let el=document.getElementById('__errbox');
if(!el){el=document.createElement('div');el.id='__errbox';el.style.cssText='position:fixed;top:0;left:0;right:0;z-index:9999;background:#7f1d1d;color:#fff;font:11px monospace;padding:8px;max-height:40vh;overflow:auto';document.body.appendChild(el);}
el.textContent=window.__errs.join('\n');
});
/* ============================== DATA ============================== */
const FEST_START=new Date(2026,9,8); /* Oct 8, 2026 — festival start (coincides with first page) */
function dayLabel(offset){
const d=new Date(FEST_START); d.setDate(d.getDate()+offset);
return {
name:d.toLocaleDateString('en-US',{weekday:'short'}),
date:d.toLocaleDateString('en-US',{month:'short',day:'numeric'}),
iso:d.toISOString().slice(0,10)
};
}
const FEST={
days:[
{ tag:'DAY 1', items:[
{id:'d1-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1080, tags:['VG'], desc:'Slow-drip single origin + oat milk latte, served from the wood-fired cart.'},
{id:'d1-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1320, tags:['V','GF'], desc:'Fresh fruit skewers, sorbet cups, and festival brownies.'},
{id:'d1-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:870, tags:['GF'], desc:'Hand-pressed corn tortillas, slow-braised grass-fed filling, house salsa.'},
{id:'d1-bbq', name:'BBQ Pit', emoji:'🍖', price:12, start:780, end:930, tags:[], desc:'12-hour offset-smoked brisket & ribs. No seed oils — rendered tallow only.'},
{id:'d1-chicken',name:'Chicken Grill', emoji:'🍗', price:9, start:1050, end:1230, tags:['GF'], desc:'Buttermilk fried chicken (tallow-fried) with slaw.'},
{id:'d1-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:1080, end:1200, tags:['V'], desc:'Fresh-cut daily pasta, garden vegetables, house olive oil.'},
{id:'d1-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1320, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'}
]},
{ tag:'DAY 2', items:[
{id:'d2-coffee', name:'Coffee Station', emoji:'☕', price:4, start:570, end:1020, tags:['VG'], desc:'Slow-drip single origin + oat milk latte.'},
{id:'d2-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1260, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'},
{id:'d2-burger', name:'Garden Burgers', emoji:'🍔', price:11, start:720, end:900, tags:[], desc:'Grass-fed beef patty, brioche, charred onion, herb aioli.'},
{id:'d2-grill', name:'Grill 484', emoji:'🔥', price:9, start:750, end:960, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'},
{id:'d2-ramen', name:'Ramen Tent', emoji:'🍜', price:12, start:1020, end:1200, tags:['V'], desc:'Miso broth simmered all day, soft egg, scallion, pickled ginger.'},
{id:'d2-drinks', name:'Drinks', emoji:'🥤', price:3, start:570, end:1290, tags:['VG'], desc:'Cold brew, kombucha, herbal tea, lemonade.'}
]},
{ tag:'DAY 3', items:[
{id:'d3-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1050, tags:['VG'], desc:'Slow-drip single origin, Saturday long pour.'},
{id:'d3-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1320, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, Saturday brownie batch.'},
{id:'d3-flatbread',name:'Flatbread Oven', emoji:'🫓', price:9, start:720, end:930, tags:['V','GF'], desc:'Wood-fired flatbreads — charred vegetables, whipped feta, house olive oil.'},
{id:'d3-bbq', name:'BBQ Pit', emoji:'🍖', price:12, start:780, end:960, tags:[], desc:'Offset-smoked brisket & ribs. The weekend is for the pit.'},
{id:'d3-dumplings',name:'Dumpling Bar', emoji:'🥟', price:10, start:1020, end:1230, tags:['GF'], desc:'Pan-seared vegetable & chicken dumplings, chili crisp.'},
{id:'d3-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1320, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'}
]},
{ tag:'DAY 4', items:[
{id:'d4-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1020, tags:['VG'], desc:'Slow-drip single origin + oat milk latte.'},
{id:'d4-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1290, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'},
{id:'d4-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:870, tags:['GF'], desc:'Corn tortillas, slow-braised grass-fed filling, house salsa.'},
{id:'d4-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:900, end:1140, tags:['V'], desc:'Sunday pasta — fresh-cut, garden vegetables, house olive oil.'},
{id:'d4-grill', name:'Grill 484', emoji:'🔥', price:9, start:960, end:1170, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'},
{id:'d4-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1290, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'}
]},
{ tag:'FINAL DAY', items:[
{id:'d5-coffee', name:'Coffee Station', emoji:'☕', price:4, start:600, end:960, tags:['VG'], desc:'One last pour. Single origin, slow drip.'},
{id:'d5-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1200, tags:['V','GF'], desc:'Sorbet, fruit, brownies — farewell batch.'},
{id:'d5-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:840, tags:['GF'], desc:'Corn tortillas, slow-braised filling.'},
{id:'d5-feast', name:'Farewell Feast · BBQ', emoji:'🍖', price:14, start:720, end:960, tags:[], desc:'The full spread: brisket, ribs, tallow fries, slaw, bread.'},
{id:'d5-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:1020, end:1170, tags:['V'], desc:'Fresh pasta, garden vegetables, olive oil.'},
{id:'d5-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1230, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'}
]}
]
};
FEST.days.forEach((d,i)=>{ const L=dayLabel(i); d.name=L.name; d.date=L.date; d.iso=L.iso; });
/* ============================== STATE ============================== */
const LS={cart:'fest484_cart',orders:'fest484_orders',btcpay:'fest484_btcpay'};
let state={
view:'schedule',
dayIdx:0,
cart:JSON.parse(localStorage.getItem(LS.cart)||'{}'),
orders:JSON.parse(localStorage.getItem(LS.orders)||'[]'),
settings:JSON.parse(localStorage.getItem(LS.btcpay)||'null'),
sheetStep:null,
btcPrice:null,
pay:null
};
const saveCart=()=>localStorage.setItem(LS.cart,JSON.stringify(state.cart));
const saveOrders=()=>localStorage.setItem(LS.orders,JSON.stringify(state.orders));
const saveSettings=()=>localStorage.setItem(LS.btcpay,JSON.stringify(state.settings));
const $=s=>document.querySelector(s);
const fmt$=n=>'$'+n.toFixed(2);
const fmtT=min=>{let h=Math.floor(min/60),m=min%60,ap=h>=12?'PM':'AM';h=h%12||12;return h+':'+String(m).padStart(2,'0')+' '+ap;};
const fmtDur=min=>{const h=Math.floor(min/60),m=Math.round(min%60);return h? (h+'h'+(m?' '+m+'m':'')) : m+'m';};
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function toast(msg){const t=$('#toast');t.textContent=msg;t.classList.add('show');clearTimeout(t._x);t._x=setTimeout(()=>t.classList.remove('show'),2200);}
function cartTotal(){let t=0;for(const id in state.cart){const it=findItem(id);if(it)t+=it.price*state.cart[id];}return t;}
function cartCount(){return Object.values(state.cart).reduce((a,b)=>a+b,0);}
function findItem(id){for(const d of FEST.days)for(const it of d.items)if(it.id===id)return it;return null;}
function findDayOf(id){for(let i=0;i<FEST.days.length;i++)if(FEST.days[i].items.some(x=>x.id===id))return i;}
/* ============================== STATUS ============================== */
function nowMin(){const d=new Date();return d.getHours()*60+d.getMinutes();}
function itemStatus(i,item){
if(i>0)return{state:'upcoming',label:FEST.days[i].name+' · '+FEST.days[i].date};
const n=nowMin();
if(n<item.start)return{state:'soon',label:'Starts in '+fmtDur(item.start-n)};
if(n<item.end)return{state:'open',label:'Available now'};
return{state:'ended',label:'Ended'};
}
/* ============================== SCHEDULE RENDER ============================== */
function renderDayTabs(){
$('#daytabs').innerHTML=FEST.days.map((d,i)=>
'<button class="daytab'+(i===state.dayIdx?' active':'')+(i===0?' today':'')+'" data-day="'+i+'">'+
'<div class="d1">'+d.tag+'</div><div class="d2">'+d.name+' '+d.date+'</div></button>').join('');
document.querySelectorAll('.daytab').forEach(b=>b.onclick=()=>{state.dayIdx=+b.dataset.day;renderDayTabs();renderTimeline();});
}
function renderTimeline(){
const day=FEST.days[state.dayIdx];
const items=[...day.items].sort((a,b)=>a.start-b.start);
const d0=Math.min(...items.map(x=>x.start))-30;
const d1=Math.max(...items.map(x=>x.end))+30;
const isToday=state.dayIdx===0;
const n=nowMin();
const nowLine=(isToday&&n>=d0&&n<=d1)
?'<div class="nowline" style="top:'+(((n-d0)/(d1-d0))*100).toFixed(2)+'%"></div>':'';
$('#tl-sub').textContent=isToday?'Live · updates automatically':'Pre-orders open';
$('#tl-wrap').innerHTML='<div class="tl-rail"></div>'+nowLine+items.map(item=>{
const st=itemStatus(state.dayIdx,item);
const cls=st.state==='open'?'is-open':st.state==='soon'?'is-soon':(st.state==='upcoming'?'':'is-ended');
const pill={open:'sp-open',soon:'sp-soon',upcoming:'sp-upcoming',ended:'sp-ended'}[st.state];
const inCart=state.cart[item.id]>0;
return '<div class="item-card '+cls+'" data-id="'+item.id+'">'+
'<div class="tl-dot"></div>'+
'<div class="tl-time">'+fmtT(item.start).replace(' ','<br>')+'</div>'+
'<div class="item-top">'+
'<div class="item-emoji">'+item.emoji+'</div>'+
'<div><div class="item-name">'+item.name+' <span class="tags">'+item.tags.map(t=>'<span class="tag">'+t+'</span>').join('')+'</span></div>'+
'<div class="item-desc">'+item.desc+'</div></div>'+
'<div class="item-price">'+fmt$(item.price)+'</div>'+
'</div>'+
'<div class="item-foot">'+
'<span class="status-pill '+pill+'"><span class="dot"></span>'+st.label+'</span>'+
'<span class="tag" style="margin-left:auto">'+fmtT(item.start)+' – '+fmtT(item.end)+'</span>'+
'<button class="add-btn'+(inCart?' in-cart':'')+'" data-add="'+item.id+'">'+(inCart?'✓ Added · '+state.cart[item.id]:'+ Add to order')+'</button>'+
'</div></div>';
}).join('');
document.querySelectorAll('[data-add]').forEach(b=>b.onclick=()=>{
const id=b.dataset.add;
state.cart[id]=(state.cart[id]||0)+1;
saveCart();renderFab();renderTimeline();
toast(findItem(id).emoji+' '+findItem(id).name+' added');
});
}
function refreshStatuses(){
document.querySelectorAll('#tl-wrap .item-card').forEach(card=>{
const id=card.dataset.id, it=findItem(id);
const st=itemStatus(state.dayIdx,it);
const pill=card.querySelector('.status-pill');
pill.className='status-pill '+( {open:'sp-open',soon:'sp-soon',upcoming:'sp-upcoming',ended:'sp-ended'}[st.state] );
pill.innerHTML='<span class="dot"></span>'+st.label;
card.classList.remove('is-open','is-soon','is-ended');
if(st.state==='open')card.classList.add('is-open');
else if(st.state==='soon')card.classList.add('is-soon');
else if(st.state==='ended')card.classList.add('is-ended');
});
}
/* ============================== FAB / NAV ============================== */
function renderFab(){
const c=cartCount();
$('#fab').classList.toggle('hidden',c===0);
$('#fab-count').textContent=c;
document.title=c>0?('KITCHEN 484 · '+c+' in cart'):('KITCHEN 484 · SOLARPUNK SUMMIT');
let d=document.getElementById('debugbar');
if(!d){d=document.createElement('div');d.id='debugbar';d.style.cssText='position:fixed;bottom:0;left:0;right:0;z-index:9998;background:#0A0A0A;color:#4ade80;font:10px monospace;padding:4px 8px;pointer-events:none';document.body.appendChild(d);}
d.textContent='cart='+JSON.stringify(state.cart)+' errs='+(window.__errs?window.__errs.length:0);
}
document.querySelectorAll('.nav-btn').forEach(b=>b.onclick=()=>{
state.view=b.dataset.view;
document.querySelectorAll('.nav-btn').forEach(x=>x.classList.toggle('active',x===b));
$('#view-schedule').hidden=state.view!=='schedule';
$('#view-orders').hidden=state.view!=='orders';
if(state.view==='orders')renderOrders();
});
$('#fab').onclick=()=>openSheet('cart');
/* ============================== SHEET STEPS ============================== */
function openSheet(step){state.sheetStep=step;renderSheet();$('#sheet-overlay').classList.add('show');}
function closeSheet(){
if(state.pay&&state.pay.timer){clearInterval(state.pay.timer);state.pay=null;}
state.sheetStep=null;$('#sheet-overlay').classList.remove('show');
}
$('#sheet-overlay').addEventListener('mousedown',e=>{$('#sheet-overlay')._down=e.target.id==='sheet-overlay';});
$('#sheet-overlay').addEventListener('click',e=>{if($('#sheet-overlay')._down&&e.target.id==='sheet-overlay')closeSheet();});
function renderSheet(){
const s=$('#sheet');
if(state.sheetStep==='cart')s.innerHTML=stepCart();
else if(state.sheetStep==='checkout')s.innerHTML=stepCheckout();
else if(state.sheetStep==='payment')s.innerHTML=stepPayment();
else if(state.sheetStep==='success')s.innerHTML=stepSuccess();
wireSheet();
}
function stepCart(){
const rows=Object.keys(state.cart).map(id=>{
const it=findItem(id);if(!it)return'';
return '<div class="cart-row" data-row="'+id+'">'+
'<div class="em">'+it.emoji+'</div>'+
'<div><div class="nm">'+it.name+'</div><div class="pt">'+fmtT(it.start)+' – '+fmtT(it.end)+'</div></div>'+
'<div class="qty"><button data-dec="'+id+'">−</button><span>'+state.cart[id]+'</span><button data-inc="'+id+'">+</button></div>'+
'<div class="lp">'+fmt$(it.price*state.cart[id])+'</div></div>';
}).join('');
const total=cartTotal();
const btc=state.btcPrice?'≈ '+(total/state.btcPrice).toFixed(6)+' BTC <small>@ $'+state.btcPrice.toFixed(0)+'</small>':'';
return '<div class="grab"></div><h3>YOUR ORDER</h3><div class="sub">SOLARPUNK SUMMIT · pickup at the stand window</div>'+
(rows||'<div class="empty" style="padding:30px"><div class="big">🧺</div>Nothing here yet — add something from the schedule.</div>')+
'<div class="total-row"><span class="t">Total</span><span class="v">'+fmt$(total)+'</span></div>'+
'<div class="btc-est">'+(btc||'Fetching BTC price…')+'</div>'+
'<button class="btn btn-primary" id="to-checkout" '+(cartCount()===0?'disabled':'')+'>Checkout →</button>';
}
function stepCheckout(){
const days=[...new Set(Object.keys(state.cart).map(findDayOf))].map(i=>FEST.days[i]);
const windows=[...new Set(days.flatMap(d=>d.items.map(it=>it.id)))]
.map(id=>({id,start:findItem(id).start,end:findItem(id).end,name:findItem(id).name}))
.sort((a,b)=>a.start-b.start);
const n=nowMin();
const def=windows.find(w=>w.end>n)||windows[0];
const opts=windows.map(w=>'<option value="'+w.start+'-'+w.end+'" '+(def&&w.start===def.start&&w.end===def.end?'selected':'')+'>'+
fmtT(w.start)+' – '+fmtT(w.end)+' · '+w.name+'</option>').join('');
return '<div class="grab"></div><h3>CHECKOUT</h3><div class="sub">Who is picking this up?</div>'+
'<div class="field"><label>Name</label><input id="co-name" type="text" placeholder="Your name at the stand" maxlength="40"></div>'+
'<div class="field"><label>Pickup window</label><select id="co-window">'+opts+'</select></div>'+
'<div class="field"><label>Notes (optional)</label><textarea id="co-notes" placeholder="Allergies, extras, anything the kitchen should know…"></textarea></div>'+
'<div class="total-row"><span class="t">Total · '+cartCount()+' items</span><span class="v">'+fmt$(cartTotal())+'</span></div>'+
'<button class="btn btn-pink" id="to-payment" style="margin-top:8px">₿&nbsp; Pay with Bitcoin</button>'+
'<button class="btn btn-ghost" style="width:100%;margin-top:8px" id="back-cart">← Back to cart</button>';
}
function stepPayment(){
const p=state.pay;
if(!p)return'<div class="grab"></div><div class="empty">Payment not started.</div>';
const addr=p.method==='bolt11'?p.bolt11:p.address;
const tabs=(p.real&&p.bolt11)?'<div class="pay-tabs">'+
'<button class="pay-tab'+(p.method==='btc'?' active':'')+'" data-pm="btc">₿ Bitcoin</button>'+
'<button class="pay-tab'+(p.method==='bolt11'?' active':'')+'" data-pm="bolt11">⚡ Lightning</button></div>':'';
return '<div class="grab"></div><h3>PAYMENT</h3>'+
'<div class="sub">'+(p.real?'BTCPay invoice '+p.invoiceId:'Demo invoice — no real transaction')+'</div>'+
(p.real?'':'<div class="demo-note">⚠️ <b>Demo mode.</b> No BTCPay Server configured — open ⚙️ settings to connect a real store. This QR is a placeholder.</div>')+
tabs+
'<div class="pay-box">'+
'<div style="font-size:11px;letter-spacing:1px;color:var(--muted);text-transform:uppercase">Send exactly</div>'+
'<div class="pay-amount">'+fmt$(p.amountUsd)+'</div>'+
'<div class="pay-btc">'+(state.btcPrice?'≈ '+(p.amountUsd/state.btcPrice).toFixed(6)+' BTC':'≈ BTC price unavailable')+'</div>'+
'<div class="qr-wrap" id="qr-wrap"></div>'+
'<div class="addr-row"><code id="pay-addr">'+esc(addr)+'</code><button class="copy-btn" id="copy-addr">Copy</button></div>'+
'</div>'+
'<div class="pay-status" id="pay-status"><span class="spinner"></span> Waiting for payment…</div>'+
(p.real?'<a class="btn btn-ghost" style="width:100%;text-decoration:none;color:var(--ink)" href="'+p.checkoutUrl+'" target="_blank" rel="noopener">Open BTCPay checkout →</a>':
'<button class="btn btn-blue" id="simulate-pay" style="margin-top:4px">⚡ Simulate payment received (demo)</button>')+
'<button class="btn btn-ghost" style="width:100%;margin-top:8px" id="cancel-pay">Cancel</button>';
}
function stepSuccess(){
const o=state.pay&&state.pay.order;
if(!o)return'';
return '<div class="grab"></div><div class="success">'+
'<div class="check">✓</div>'+
'<h3>ORDER CONFIRMED</h3>'+
'<div class="sub" style="margin-bottom:0">'+(o.status==='PAID (DEMO)'?'Demo payment recorded — connect BTCPay for real orders':'Payment detected')+'</div>'+
'<div class="code">'+o.code+'</div>'+
'<div class="pick">Pickup: <b>'+o.pickupLabel+'</b><br>'+o.itemsSummary+'</div>'+
'<div class="pay-amount" style="font-size:20px">'+fmt$(o.totalUsd)+'</div>'+
'<div class="pay-btc" style="margin-bottom:18px">'+(o.btcEstimate||'')+'</div>'+
'<button class="btn btn-primary" id="success-done">Done</button>'+
'</div>';
}
function wireSheet(){
const s=$('#sheet');
s.querySelectorAll('[data-inc]').forEach(b=>b.onclick=()=>{state.cart[b.dataset.inc]++;afterCartChange();});
s.querySelectorAll('[data-dec]').forEach(b=>b.onclick=()=>{
const id=b.dataset.dec;state.cart[id]--;
if(state.cart[id]<=0)delete state.cart[id];
afterCartChange();
});
const tc=s.querySelector('#to-checkout');if(tc)tc.onclick=()=>openSheet('checkout');
const bc=s.querySelector('#back-cart');if(bc)bc.onclick=()=>openSheet('cart');
const tp=s.querySelector('#to-payment');
if(tp)tp.onclick=async()=>{
const name=$('#co-name').value.trim();
if(!name){toast('Please add a pickup name');$('#co-name').focus();return;}
const win=$('#co-window').value.split('-').map(Number);
const notes=$('#co-notes').value.trim();
await startPayment({name,window:win,notes});
};
s.querySelectorAll('[data-pm]').forEach(b=>b.onclick=()=>{
state.pay.method=b.dataset.pm;
const addr=state.pay.method==='bolt11'?state.pay.bolt11:state.pay.address;
$('#pay-addr').textContent=addr;
s.querySelectorAll('[data-pm]').forEach(x=>x.classList.toggle('active',x===b));
drawQR($('#qr-wrap'),addr);
});
const cp=s.querySelector('#copy-addr');
if(cp)cp.onclick=async()=>{
const t=$('#pay-addr').textContent;
try{await navigator.clipboard.writeText(t);toast('Copied');}
catch(e){const ta=document.createElement('textarea');ta.value=t;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();toast('Copied');}
};
const sim=s.querySelector('#simulate-pay');
if(sim)sim.onclick=()=>completeOrder('PAID (DEMO)');
const cx=s.querySelector('#cancel-pay');
if(cx)cx.onclick=()=>{
if(state.pay&&state.pay.timer)clearInterval(state.pay.timer);
state.pay=null;openSheet('checkout');
};
const done=s.querySelector('#success-done');
if(done)done.onclick=()=>{
closeSheet();
document.querySelector('[data-view="orders"]').click();
};
}
function afterCartChange(){saveCart();renderFab();renderTimeline();renderSheet();}
/* ============================== QR ============================== */
function drawQR(el,data){
if(!el)return;
el.innerHTML='';
try{
if(typeof QRCode!=='undefined'){
new QRCode(el,{text:data,width:190,height:190,colorDark:'#0A0A0A',colorLight:'#ffffff',correctLevel:QRCode.CorrectLevel.M});
}else{throw new Error('no lib');}
}catch(e){
el.innerHTML='<div style="width:190px;height:190px;display:grid;place-items:center;background:var(--bg2);border-radius:8px;color:var(--muted);font-size:11px">QR unavailable<br>offline</div>';
}
}
/* ============================== PAYMENT ============================== */
function demoAddress(){
const chars='023456789acdefghjklmnpqrstuvwxyz';
let s='bc1q';
for(let i=0;i<38;i++)s+=chars[Math.floor(Math.random()*chars.length)];
return s;
}
async function fetchBtcPrice(){
try{
const r=await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd',{cache:'no-store'});
const j=await r.json();
if(j.bitcoin&&j.bitcoin.usd){state.btcPrice=j.bitcoin.usd;return;}
}catch(e){}
if(!state.btcPrice)state.btcPrice=65000; // offline fallback
}
function btcpayHeaders(){
const s=state.settings||{};
return s.apiKey?{'Authorization':'Bearer '+s.apiKey}:{};
}
async function startPayment(info){
const usd=cartTotal();
const day=FEST.days.find(d=>d.items.some(it=>info.window&&Math.abs(it.start-info.window[0])<1&&Math.abs(it.end-info.window[1])<1))||FEST.days[state.dayIdx];
const pickupLabel=fmtT(info.window[0])+' – '+fmtT(info.window[1]);
const itemsSummary=Object.keys(state.cart).map(id=>{const it=findItem(id);return state.cart[id]+'× '+it.name;}).join(', ');
const order={
id:crypto.randomUUID?crypto.randomUUID():String(Date.now()),
code:'F484-'+Math.random().toString(36).slice(2,6).toUpperCase(),
name:info.name,window:info.window,notes:info.notes,
items:JSON.parse(JSON.stringify(state.cart)),
itemsSummary, totalUsd:usd,
btcEstimate:state.btcPrice?((usd/state.btcPrice).toFixed(6)+' BTC'):null,
pickupLabel, day:day.name+' '+day.date,
status:'PENDING', ts:Date.now(), real:false, address:null
};
const p=state.pay={order,amountUsd:usd,method:'btc',address:null,bolt11:null,real:false,invoiceId:null,checkoutUrl:null,timer:null};
const s=state.settings;
if(s&&s.url&&s.store){
try{
const res=await fetch(s.url.replace(/\/+$/,'')+'/v1/stores/'+s.store+'/invoices',{
method:'POST',
headers:Object.assign({'Content-Type':'application/json'},btcpayHeaders()),
body:JSON.stringify({
amount:Math.round(usd*100)/100, currency:'USD',
description:'KITCHEN 484 order '+order.code+' — pickup '+pickupLabel,
metadata:{orderId:order.code,pickup:pickupLabel}
})
});
if(!res.ok)throw new Error('HTTP '+res.status);
const inv=await res.json();
p.real=true;order.real=true;
p.invoiceId=inv.id;
p.checkoutUrl=inv.checkoutUrl||inv.paymentUrl||'';
let addr=null,bolt11=null;
if(Array.isArray(inv.paymentMethods)){
const btcPM=(inv.paymentMethods.find(pm=>pm.cryptoCode==='BTC')||{});
const lnPM=(inv.paymentMethods.find(pm=>pm.cryptoCode==='LIGHTNING')||{});
addr=btcPM.address||null; bolt11=lnPM.bolt11||lnPM.address||null;
}
if(!addr&&inv.paymentAddresses){
addr=typeof inv.paymentAddresses==='string'?inv.paymentAddresses:(inv.paymentAddresses.BTC||null);
}
if(!addr)throw new Error('No BTC address in invoice');
p.address=addr;order.address=addr;p.bolt11=bolt11;
startPolling(order);
}catch(e){
p.real=false;order.real=false;
p.address=demoAddress();order.address=p.address;
toast('BTCPay unavailable ('+e.message+') — demo invoice shown');
}
}else{
p.address=demoAddress();order.address=p.address;
}
openSheet('payment');
}
function startPolling(order){
if(state.pay.timer)clearInterval(state.pay.timer);
const s=state.settings;
let ticks=0;
state.pay.timer=setInterval(async()=>{
ticks++;
try{
const r=await fetch(s.url.replace(/\/+$/,'')+'/v1/invoices/'+state.pay.invoiceId,{headers:btcpayHeaders()});
if(!r.ok)return;
const inv=await r.json();
const st=(inv.status||'').toLowerCase();
if(['paid','confirmed','settled'].includes(st)){
clearInterval(state.pay.timer);state.pay.timer=null;
completeOrder('PAID');
}else if(st==='expired'||st==='failed'){
clearInterval(state.pay.timer);state.pay.timer=null;
const ps=$('#pay-status');
if(ps){ps.className='pay-status err';ps.textContent='Invoice expired — please go back and retry.';}
}
}catch(e){ if(ticks%12===0){const ps=$('#pay-status');if(ps)ps.textContent='Still waiting… (reconnecting)';} }
},5000);
}
function completeOrder(status){
const p=state.pay;if(!p||!p.order)return;
clearInterval(p.timer);p.timer=null;
p.order.status=status;
if(!state.orders.some(o=>o.id===p.order.id))state.orders.unshift(p.order);
saveOrders();
state.cart={};saveCart();renderFab();renderTimeline();
openSheet('success');
}
/* ============================== ORDERS VIEW ============================== */
function renderOrders(){
const list=$('#orders-list');
if(!state.orders.length){
list.innerHTML='<div class="empty"><div class="big">🧾</div>No orders yet.<br>Check the schedule and grab something good.</div>';
return;
}
list.innerHTML=state.orders.map(o=>{
const badge=o.status==='PAID'?'<span class="badge b-paid">PAID</span>'
:o.status==='PAID (DEMO)'?'<span class="badge b-demo">PAID · DEMO</span>'
:o.status==='FAILED'?'<span class="badge b-failed">FAILED</span>'
:'<span class="badge b-pending">PENDING</span>';
return '<div class="order-card">'+
'<div class="order-top"><span class="order-code">'+o.code+'</span>'+badge+'</div>'+
'<div class="order-meta">'+o.day+' · pickup '+o.pickupLabel+' · '+esc(o.name)+(o.notes?' · “'+esc(o.notes)+'”':'')+'</div>'+
'<div class="order-items">'+o.itemsSummary+'</div>'+
'<div class="order-total"><span>'+fmt$(o.totalUsd)+'</span><span class="btc">'+(o.btcEstimate||'')+'</span></div>'+
(o.real&&o.status!=='PAID'&&o.status!=='FAILED'?'<div class="order-actions"><button class="btn btn-ghost btn-sm" data-recheck="'+o.id+'">Re-check status</button></div>':'')+
'</div>';
}).join('');
list.querySelectorAll('[data-recheck]').forEach(b=>b.onclick=async()=>{
const o=state.orders.find(x=>x.id===b.dataset.recheck);
const s=state.settings;if(!s)return;
b.disabled=true;b.textContent='Checking…';
try{
const r=await fetch(s.url.replace(/\/+$/,'')+'/v1/invoices/'+o.invoiceId,{headers:btcpayHeaders()});
const inv=await r.json();
const st=(inv.status||'').toLowerCase();
if(['paid','confirmed','settled'].includes(st)){o.status='PAID';saveOrders();toast('Payment confirmed 🎉');}
}catch(e){}
renderOrders();
});
}
/* ============================== SETTINGS ============================== */
$('#btn-settings').onclick=()=>{
const s=state.settings||{};
$('#set-url').value=s.url||'';$('#set-store').value=s.store||'';$('#set-key').value=s.apiKey||'';
const tr=$('#test-result');tr.className='test-result';
$('#settings-modal').classList.add('show');
};
$('#btn-close-settings').onclick=()=>$('#settings-modal').classList.remove('show');
$('#settings-modal').addEventListener('mousedown',e=>{$('#settings-modal')._down=e.target.id==='settings-modal';});
$('#settings-modal').addEventListener('click',e=>{if(e.target.id==='settings-modal'&&$('#settings-modal')._down)e.target.classList.remove('show');});
$('#btn-save-settings').onclick=()=>{
state.settings={url:$('#set-url').value.trim(),store:$('#set-store').value.trim(),apiKey:$('#set-key').value.trim()};
saveSettings();
$('#settings-modal').classList.remove('show');
toast(state.settings.url?'BTCPay saved':'Settings cleared');
};
/* Legacy direct-BTCPay test handler retained below only as migration reference.
$('#btn-test').onclick=async()=>{
const tr=$('#test-result');
tr.className='test-result';
tr.textContent='Testing payment proxy…';
try{
const r=await fetch(url.replace(/\/+$/,'')+'/v1/stores/'+store,{headers:$('#set-key').value.trim()?{'Authorization':'Bearer '+$('#set-key').value.trim()}:{}});
if(r.ok){
const j=await r.json();
tr.className='test-result ok';tr.textContent='✓ Connected — store: '+(j.name||j.id);
}else{
tr.className='test-result err';tr.textContent='✗ HTTP '+r.status+(r.status===401?' — add a store API key with read access.':'');
}
}catch(e){tr.className='test-result err';tr.textContent='✗ Could not reach server: '+e.message;}
};
*/
/* Test the local proxy; BTCPay credentials must never be sent from this page. */
$('#btn-test').onclick=async()=>{
const tr=$('#test-result');tr.className='test-result';tr.textContent='Testing payment proxy…';
try{
const r=await fetch('/api/settings');
const j=await r.json().catch(()=>({}));
if(!r.ok)throw new Error('HTTP '+r.status);
tr.className=j.configured?'test-result ok':'test-result err';
tr.textContent=j.configured?'✓ Payment proxy connected — '+(j.store||'store configured'):'✗ Payment proxy is not configured.';
}catch(e){tr.className='test-result err';tr.textContent='✗ Could not reach payment proxy: '+e.message;}
};
$('#btn-clear-orders').onclick=()=>{
if(!state.orders.length)return;
if(confirm('Clear all saved orders?')){state.orders=[];saveOrders();$('#settings-modal').classList.remove('show');renderOrders();}
};
/* ============================== THEME (White ⇄ Cosmic) ============================== */
const THEME_KEY='kitchen484-theme';
function applyTheme(t){
t=(t==='cosmic')?'cosmic':'white';
if(t==='cosmic')document.documentElement.setAttribute('data-theme','cosmic');
else document.documentElement.removeAttribute('data-theme');
const b=$('#btn-theme');
if(b){b.textContent=(t==='cosmic')?'☾ WHITE':'✦ COSMIC';b.title=(t==='cosmic')?'Switch to white theme':'Switch to cosmic theme';}
const meta=document.querySelector('meta[name="theme-color"]');
if(meta)meta.setAttribute('content',t==='cosmic'?'#0d0a06':'#FFFFFF');
}
function currentTheme(){return document.documentElement.getAttribute('data-theme')==='cosmic'?'cosmic':'white';}
function initTheme(){
applyTheme('white');
const b=$('#btn-theme');
if(b)b.onclick=()=>{const next=currentTheme()==='cosmic'?'white':'cosmic';applyTheme(next);try{localStorage.setItem(THEME_KEY,next);}catch(e){}};
}
initTheme();
/* ============================== CLOCK / LOOP ============================== */
function tickClock(){
const d=new Date();
$('#clock').textContent=String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');
}
setInterval(()=>{tickClock();if(!$('#view-schedule').hidden)refreshStatuses();},30000);
/* ============================== INIT ============================== */
(async function init(){
tickClock();
renderDayTabs();
renderTimeline();
renderFab();
try{
const r=await fetch('/api/settings');
if(r.ok){const j=await r.json();if(j.configured){state.settings={configured:true,url:j.url,store:j.store};saveSettings();}}
}catch(e){}
await fetchBtcPrice();
setInterval(fetchBtcPrice,5*60*1000);
})();
</script>
</body>
</html>