Files
site/index.html
T

406 lines
34 KiB
HTML
Raw 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, viewport-fit=cover">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src https://api.venice.ai https://inference.yolopreneur.lol; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src blob: data:; font-src 'self'">
<title>g*ndora</title>
<meta name="description" content="Type a seed. Get a station. Every song is made for you as it plays, and every cover is drawn from the song's own settings.">
<link rel="icon" href="favicon.svg">
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#0b0b0e">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="gandora">
<style>
:root{
--bg:#0b0b0e; --ink:#ededf0; --dim:#8a8a96; --line:#232329; --card:#121216; --field:#0a0a0d;
--acc:var(--ink); --onacc:var(--bg); --veil:rgba(11,11,14,.55); --veil2:rgba(11,11,14,.65); --shade:8,8,10;
--font:ui-monospace,"SF Mono",Menlo,Consolas,monospace; --sans:-apple-system,"Inter",system-ui,sans-serif;
color-scheme:dark;
}
:root[data-theme="light"]{
--bg:#f4f4f6; --ink:#17171a; --dim:#6b6b76; --line:#dcdce2; --card:#ffffff; --field:#ffffff;
--veil:rgba(244,244,246,.6); --veil2:rgba(244,244,246,.75); --shade:244,244,246; color-scheme:light;
}
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){
--bg:#f4f4f6; --ink:#17171a; --dim:#6b6b76; --line:#dcdce2; --card:#ffffff; --field:#ffffff;
--veil:rgba(244,244,246,.6); --veil2:rgba(244,244,246,.75); --shade:244,244,246; color-scheme:light;
}}
*{box-sizing:border-box} html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.5 var(--sans);-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:inherit}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
input,textarea{font:inherit;color:inherit}
.hidden{display:none!important}
/* ---------- seed screen ---------- */
#seed{min-height:100dvh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px 16px;position:relative}
#seed .wordmark{position:absolute;top:calc(26px + env(safe-area-inset-top));left:0;right:0;text-align:center;font:600 15px var(--font);letter-spacing:.18em;color:var(--ink)}
#seed .wordmark .ast{color:var(--ink);font-weight:700}
#seed form{width:min(680px,100%);text-align:center}
#seedIn{width:100%;background:transparent;border:0;border-bottom:1px solid var(--line);padding:14px 4px;text-align:center;font:400 clamp(24px,5vw,38px)/1.2 var(--sans);outline:none;caret-color:var(--acc);transition:border-color .3s}
#seedIn:focus{border-color:var(--acc)}
#seedIn::placeholder{color:#3a3a44}
#seed .hint{margin-top:18px;color:var(--dim);font-size:13px;min-height:1.5em}
#seedCover{width:120px;height:120px;margin:0 auto 28px;border-radius:14px;overflow:hidden;background:var(--card);opacity:0;transform:scale(.92);transition:opacity .5s,transform .5s}
#seedCover.on{opacity:1;transform:none}
#seedCover canvas{width:100%;height:100%;display:block}
#nowRow{position:absolute;top:44px;left:0;right:0;display:flex;justify-content:center}
#now{display:flex;align-items:center;gap:10px;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:6px 14px 6px 6px;font-size:13px;cursor:pointer;max-width:calc(100% - 32px)}
#now canvas{width:28px;height:28px;border-radius:50%;display:block}
#now span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#now b{font:600 11px var(--font);letter-spacing:.14em;color:var(--dim);margin-left:4px}
#stations{width:min(680px,100%);margin-top:40px}
#stations h3{font:600 11px var(--font);letter-spacing:.2em;color:var(--dim);margin:0 0 12px;text-align:center}
#stations .row{display:flex;gap:14px;overflow-x:auto;padding:4px 4px 10px;scrollbar-width:none;justify-content:safe center}
#stations .s{flex:0 0 96px;cursor:pointer;text-align:center}
#stations .s canvas{width:72px;height:72px;border-radius:50%;display:block;margin:0 auto 6px;background:var(--card)}
#stations .s.on canvas{outline:2px solid var(--acc);outline-offset:3px}
#stations .s b{display:block;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#stations .s small{color:var(--dim);font-size:11px}
#keyRow{position:absolute;bottom:18px;left:0;right:0;text-align:center;font-size:12px;color:var(--dim);display:flex;gap:18px;justify-content:center;flex-wrap:wrap}
#keyRow button{text-decoration:underline;text-underline-offset:3px}
#keyRow .nav{font:600 11px var(--font);letter-spacing:.18em;text-decoration:none}
#keyBox{position:absolute;bottom:44px;left:50%;transform:translateX(-50%);width:min(420px,calc(100% - 32px));background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px;font-size:13px}
#keyBox input{width:100%;margin:8px 0;background:var(--field);border:1px solid var(--line);border-radius:8px;padding:9px 10px;font-family:var(--font);font-size:12px;outline:none}
#keyBox .row{display:flex;gap:8px;justify-content:flex-end}
#keyBox .row button{padding:6px 12px;border-radius:8px;border:1px solid var(--line)}
#keyBox .row button.go{background:var(--acc);color:var(--onacc);border-color:var(--acc)}
#keyBox p{margin:0 0 4px;color:var(--dim)}
#keyBox label{display:block;margin-top:6px;color:var(--dim)}
/* ---------- player ---------- */
#player{min-height:100dvh;display:grid;grid-template-rows:auto 1fr auto;padding:calc(22px + env(safe-area-inset-top)) 16px calc(16px + env(safe-area-inset-bottom))}
#top{display:flex;align-items:center;justify-content:space-between;font:600 12px var(--font);letter-spacing:.2em;color:var(--dim)}
#stage .station{font:600 16px var(--font);letter-spacing:.18em;color:var(--ink);max-width:min(86vw,420px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;text-transform:uppercase;margin-bottom:-2px}
#stage .station:hover{color:var(--dim)}
#stage .station:hover{color:var(--acc)}
#stations .s{position:relative}
#stations .s .dots{position:absolute;top:-4px;right:2px;width:22px;height:22px;border-radius:50%;background:var(--card);border:1px solid var(--line);color:var(--dim);font-size:12px;line-height:1}
#scrim{position:fixed;inset:0;z-index:5;background:rgba(0,0,0,.25)}
#stationSheet,#picker,#tapePick{z-index:6}
#stationSheet{position:fixed;left:50%;bottom:0;transform:translateX(-50%);width:min(480px,100%);max-height:88vh;max-height:88dvh;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;background:var(--card);border:1px solid var(--line);border-bottom:0;border-radius:18px 18px 0 0;padding:14px 14px calc(14px + env(safe-area-inset-bottom));z-index:6;box-shadow:0 -20px 60px rgba(0,0,0,.6)}
#stationSheet details{margin-top:10px}#stationSheet summary{cursor:pointer;color:var(--dim);font-size:12px;list-style:none;display:flex;align-items:center;gap:6px}#stationSheet summary::-webkit-details-marker{display:none}#stationSheet summary::before{content:"▸";font-size:10px}#stationSheet details[open] summary::before{content:"▾"}
#stationSheet h4{font:600 10px var(--font);letter-spacing:.2em;color:var(--dim);margin:0 0 10px}
#upd{position:fixed;top:calc(10px + env(safe-area-inset-top));left:50%;transform:translateX(-50%);z-index:9;padding:9px 16px;border-radius:999px;background:var(--ink);color:var(--bg);font:600 11px var(--font);letter-spacing:.16em;box-shadow:0 10px 30px -10px rgba(0,0,0,.6)}
#stationSheet .x{position:sticky;top:-8px;float:right;margin:-8px -6px 0 0;z-index:1;background:var(--card);width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--dim);font:300 22px var(--sans);line-height:1}
#stationSheet .x:hover{color:var(--ink);background:var(--bg)}
#stationSheet label{display:block;color:var(--dim);font-size:12px;margin:8px 0 4px}
#stationSheet input{width:100%;background:var(--field);border:1px solid var(--line);border-radius:10px;padding:10px 12px;outline:none}
#stationSheet .row{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
#stationSheet .sl{display:flex;align-items:center;gap:12px;margin-top:6px}
#stationSheet .sl label{margin:0;width:84px}
#stationSheet input[type=range]{flex:1;accent-color:var(--ink);margin:0}
#stationSheet .sl span{font:11px var(--font);color:var(--dim);width:32px;text-align:right}
#stationSheet .row button{padding:9px 14px;border-radius:999px;border:1px solid var(--line);font:600 11px var(--font);letter-spacing:.14em}
#stationSheet .row button.go{background:var(--acc);color:var(--onacc);border-color:var(--acc)}
#stationSheet .row button.bad{color:hsl(0 60% 70%);border-color:hsl(0 30% 35%)}
#top button{color:var(--dim);letter-spacing:.08em}
#stage{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:14px 0}
/* the flow: station, cover (trail on hover), transport, title, bar, then the DJ line, NEXT, lyrics/keep/share, tags */
#stationName{order:0}#cover{order:1}#controls{order:2}#meta{order:4}#bar{order:5}#meta2{order:7}
#stage #controls{margin-top:2px}
#meta2 #pills{margin:10px 0 8px}
#cover{width:min(78vw,380px);aspect-ratio:1;position:relative;perspective:1200px}
#cover .card{position:absolute;inset:0;transform-style:preserve-3d;transition:transform .7s cubic-bezier(.4,.1,.2,1)}
#cover.flipped .card{transform:rotateY(180deg)}
#cover .face{position:absolute;inset:0;border-radius:18px;overflow:hidden;background:var(--card);box-shadow:0 30px 80px -30px rgba(0,0,0,.9);backface-visibility:hidden;-webkit-backface-visibility:hidden}
#cover .back{transform:rotateY(180deg);background:var(--card);border:1px solid var(--line);display:flex;flex-direction:column}
#cover canvas{width:100%;height:100%;display:block}
#cover .back .backTitle{font:600 10px var(--font);letter-spacing:.22em;color:var(--dim);padding:14px 16px 6px;text-align:center}
#cover .veil{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:var(--veil);backdrop-filter:blur(2px);font:12px var(--font);letter-spacing:.2em;color:var(--ink);opacity:0;transition:opacity .3s;pointer-events:none}
#cover.cooking .veil,#cover.tap .veil{opacity:1}
#meta{text-align:center;max-width:520px}
#title{font-size:20px;font-weight:600;letter-spacing:-.01em;margin:0}
#tags{color:var(--dim);font:11px var(--font);margin:4px 0 0;letter-spacing:.06em}
#lyric{color:var(--dim);font-size:13px;margin:8px 0 0;min-height:1.5em;font-style:italic;cursor:text}
#lyric:hover{color:var(--ink)}
#meta2{text-align:center;max-width:560px;width:100%}
#meta2 #lyric{margin-top:0}
#tagRow{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:10px auto 0;max-width:560px}
#tagList{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:8px auto 0;max-width:560px}
#tagList:empty{display:none}
#tagList button{font:12px var(--font);letter-spacing:.02em;padding:5px 10px;border-radius:999px;border:1px solid var(--line);color:var(--dim);background:transparent;transition:all .15s}
#tagList button.like{color:var(--onacc);background:var(--acc);border-color:var(--acc)}
#tagList button.avoid{color:hsl(220 40% 70%);border-color:hsl(220 30% 40%);text-decoration:line-through;opacity:.8}
#tagList button.add{border-style:dashed}
#tagList input{font:12px var(--font);width:110px;padding:5px 10px;border-radius:999px;border:1px solid var(--acc);background:var(--card);outline:none}
#meta2 #next{margin:8px 0 0}
#tagRow button.meta{color:var(--dim);border-style:dotted;cursor:default}
#tagRow button.fold{color:var(--acc);border-color:transparent;font-weight:600;letter-spacing:.12em}
#stage{gap:14px}
#tagRow button{font:12px var(--font);letter-spacing:.02em;padding:5px 10px;border-radius:999px;border:1px solid var(--line);color:var(--dim);background:transparent;transition:all .15s}
#tagRow button.like{color:var(--onacc);background:var(--acc);border-color:var(--acc)}
#tagRow button.avoid{color:hsl(220 40% 70%);border-color:hsl(220 30% 40%);text-decoration:line-through;opacity:.8}
#tagRow button.add{border-style:dashed}
#tagRow input{font:12px var(--font);width:110px;padding:5px 10px;border-radius:999px;border:1px solid var(--acc);background:var(--card);outline:none}
#lyrics{white-space:pre-wrap;text-align:center;font-size:13px;line-height:1.65;margin:0;color:var(--ink);flex:1;overflow:auto;padding:4px 16px 18px;font-family:var(--sans);-webkit-mask-image:linear-gradient(#000 85%,transparent);mask-image:linear-gradient(#000 85%,transparent)}
#lyrics.none{color:var(--dim);font-style:italic;display:grid;place-items:center}
#lyrics .sec{display:block;color:var(--dim);font:600 10px var(--font);letter-spacing:.2em;margin:12px 0 2px}
#lyrics .sec:first-child{margin-top:0}
#cover{cursor:pointer}
#bar{width:min(78vw,380px);height:2px;background:var(--line);border-radius:2px;position:relative;cursor:pointer;touch-action:none}
#bar::before{content:"";position:absolute;left:0;right:0;top:-14px;bottom:-14px} /* a fat finger target around a thin line */
#bar i{display:block;height:100%;width:0;background:var(--acc);border-radius:2px;pointer-events:none}
#bar:hover,#bar.drag{height:4px;margin:-1px 0}
#bar em{position:absolute;top:-20px;transform:translateX(-50%);font-size:11px;font-style:normal;line-height:1;cursor:pointer;opacity:.95;white-space:nowrap;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:2px 5px}
#bar em.below{top:auto;bottom:-20px}
#bar em:hover{border-color:var(--ink)}
#bar b{position:absolute;top:-28px;transform:translateX(-50%);font:11px var(--font);letter-spacing:.1em;color:var(--dim);background:var(--card);border:1px solid var(--line);border-radius:6px;padding:2px 6px;pointer-events:none;display:none;white-space:nowrap}
#bar.drag b{display:block}
#controls{display:flex;align-items:center;gap:18px}
#controls button{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;color:var(--ink);opacity:.85}
#controls #backTrack svg,#controls #skip svg{width:24px;height:24px}
#controls #prev{opacity:.6}
#pills{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
#pills button{display:inline-flex;align-items:center;gap:8px;padding:9px 18px;border-radius:999px;border:1px solid var(--line);font:600 12px var(--font);letter-spacing:.14em;color:var(--ink);background:var(--card);transition:background .2s,color .2s,border-color .2s}
#pills button.on{background:var(--acc);color:var(--onacc);border-color:var(--acc)}
#pills button:disabled{opacity:.4;cursor:default}
#controls button:hover{opacity:1}
#controls #play{width:62px;height:62px;background:var(--ink);color:var(--bg)}
/* the trail: a strip along the bottom of the cover; shows on hover (always on touch screens) */
#trail{position:absolute;left:0;right:0;bottom:0;z-index:3;display:flex;gap:10px;align-items:flex-end;overflow-x:auto;padding:40px 14px 14px;border-radius:0 0 18px 18px;scrollbar-width:none;-webkit-overflow-scrolling:touch;
background:linear-gradient(to top,rgba(var(--shade),.92),rgba(var(--shade),.55) 60%,transparent);opacity:0;pointer-events:none;transition:opacity .25s}
#cover:hover #trail,#cover.flipped:hover #trail{opacity:1;pointer-events:auto}
@media (hover:none){#trail{opacity:1;pointer-events:auto;padding-top:18px}}
#trail::-webkit-scrollbar{display:none}
#trail:empty{display:none}
#trail canvas{width:64px;height:64px;border-radius:12px;flex:none;opacity:.6;cursor:pointer;transition:opacity .2s,transform .2s}
#trail canvas:hover{opacity:.95;transform:translateY(-3px)}
#trail canvas.on{opacity:1;transform:scale(1.12);box-shadow:0 0 0 2px var(--ink)}
#trail canvas.kept{box-shadow:0 0 0 2px var(--acc)}
#trail canvas.on.kept{box-shadow:0 0 0 2px var(--acc),0 0 0 4px var(--ink)}
#controls svg{width:22px;height:22px;fill:currentColor}
#controls #loop,#controls #shuffle,#controls #remix{opacity:.6}
#controls #remix:hover{opacity:1}
#controls #shuffle.on{opacity:1;color:var(--acc);background:rgba(233,216,166,.12)}
#controls #loop svg{width:18px;height:18px}
#controls #loop.on{opacity:1;color:var(--onacc);background:var(--acc);border-radius:999px}
#next{font:11px var(--font);letter-spacing:.14em;color:var(--dim);min-height:1.4em;text-align:center}
#react{display:flex;flex-direction:column;gap:10px;align-items:center}
#emojis{display:flex;gap:6px;justify-content:center;flex-wrap:wrap}
#emojis button{font-size:22px;width:44px;height:44px;border-radius:12px;border:1px solid transparent;transition:transform .15s,border-color .15s,background .15s}
#emojis button:hover{background:var(--card);border-color:var(--line)}
#emojis button.hit{transform:scale(1.25);border-color:var(--acc)}
#emojis #more{font-size:20px;color:var(--dim);background:var(--card);border:1px solid var(--line)}
#picker{position:fixed;left:50%;bottom:0;transform:translateX(-50%);width:min(560px,100%);max-height:55dvh;background:var(--card);border:1px solid var(--line);border-bottom:0;border-radius:18px 18px 0 0;padding:12px 12px calc(12px + env(safe-area-inset-bottom));overflow:auto;z-index:6;box-shadow:0 -20px 60px rgba(0,0,0,.6)}
#picker h4{font:600 10px var(--font);letter-spacing:.2em;color:var(--dim);margin:10px 4px 6px}
#picker .g{display:grid;grid-template-columns:repeat(auto-fill,minmax(42px,1fr));gap:2px}
#picker .g button{font-size:24px;height:42px;border-radius:10px}
#picker .g button:hover{background:#1a1a20}
#picker .top{display:flex;gap:8px;align-items:center;margin-bottom:4px}
#picker .top input{flex:1;background:#0a0a0d;border:1px solid var(--line);border-radius:10px;padding:8px 12px;font-size:16px;outline:none}
#picker .top button{padding:8px 12px;border-radius:10px;border:1px solid var(--line);color:var(--dim);font:600 11px var(--font);letter-spacing:.14em}
#sayRow{display:flex;gap:8px;width:min(520px,100%)}
#say{flex:1;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:10px 14px;outline:none;font-size:14px}
#say:focus{border-color:var(--acc)}
#sayRow button{padding:0 14px;border:1px solid var(--line);border-radius:12px;color:var(--dim)}
#toast{position:fixed;left:50%;bottom:110px;transform:translateX(-50%);background:var(--card);border:1px solid var(--line);border-radius:10px;padding:8px 14px;font-size:13px;opacity:0;transition:opacity .25s;pointer-events:none;max-width:calc(100% - 32px);text-align:center}
#toast.on{opacity:1}
/* ---------- taste ---------- */
#taste{position:fixed;inset:0;background:rgba(11,11,14,.97);padding:18px 16px calc(18px + env(safe-area-inset-bottom));overflow:auto}
#taste header{display:flex;justify-content:space-between;align-items:center;font:600 12px var(--font);letter-spacing:.2em;color:var(--dim);margin-bottom:12px}
#tasteTabs{display:flex;gap:8px;overflow-x:auto;padding:2px 0 12px;scrollbar-width:none}
#tasteTabs button{display:flex;align-items:center;gap:8px;padding:6px 12px 6px 6px;border-radius:999px;border:1px solid var(--line);font-size:12px;white-space:nowrap;color:var(--dim)}
#tasteTabs button.on{color:var(--ink);border-color:var(--acc)}
#tasteTabs canvas{width:22px;height:22px;border-radius:50%}
#tasteMap{width:100%;max-width:640px;aspect-ratio:1;display:block;margin:0 auto;border-radius:18px;background:var(--card)}
#brainMap{width:100%;max-width:640px;aspect-ratio:2.2;display:block;margin:0 auto 14px;border-radius:18px;background:var(--card)}
#tastePalette{max-width:640px;margin:6px auto 18px}
#tastePalette h3{font:600 11px var(--font);letter-spacing:.2em;color:var(--dim);margin:0 0 10px;text-align:center}
#tastePalette .row{display:flex;gap:14px;overflow-x:auto;padding:4px;scrollbar-width:none;justify-content:safe center}
#tastePalette .s{flex:0 0 96px;cursor:pointer;text-align:center}
#tastePalette .s canvas{width:80px;height:80px;border-radius:14px;display:block;margin:0 auto 6px}
#tastePalette .s b{display:block;font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#tasteNote{color:var(--dim);font-size:12px;text-align:center;margin:12px auto 0;max-width:520px}
#tasteLegend{display:flex;justify-content:center;gap:16px;font:11px var(--font);letter-spacing:.08em;color:var(--dim);margin-top:10px;flex-wrap:wrap}
/* ---------- tapes ---------- */
#lib .t .addTape{position:absolute;right:6px;bottom:6px;width:26px;height:26px;border-radius:50%;background:rgba(11,11,14,.75);border:1px solid var(--line);color:var(--ink);font-size:16px;line-height:1;display:grid;place-items:center}
#lib .t .addTape:hover{background:var(--acc);color:var(--onacc)}
#lib .t .fromT{position:absolute;left:6px;top:6px;width:26px;height:26px;border-radius:50%;background:var(--veil2);border:1px solid var(--line);color:var(--ink);font:700 16px var(--font);line-height:1;display:grid;place-items:center;opacity:0;transition:opacity .2s}
#lib .t:hover .fromT{opacity:1}
@media (hover:none){#lib .t .fromT{opacity:.8}}
#lib .t .fromT:hover{background:var(--acc);color:var(--onacc)}
#tapeRow{display:flex;gap:14px;overflow-x:auto;padding:4px 4px 10px;scrollbar-width:none;grid-column:1/-1}
#tapeRow .s{flex:0 0 110px;cursor:pointer;text-align:center}
#tapeRow .s .tc{width:88px;height:88px;border-radius:14px;margin:0 auto 6px;position:relative;overflow:hidden;background:var(--card);display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;gap:2px}
#tapeRow .s .tc canvas{width:100%;height:100%;display:block}
#tapeRow .s .tc.one{grid-template-columns:1fr;grid-template-rows:1fr}
#tapeRow .s.new .tc{display:grid;place-items:center;border:1px dashed var(--line);font-size:26px;color:var(--dim)}
#tapeRow .s b{display:block;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#tapeRow .s small{color:var(--dim);font-size:11px}
#tapePick{position:fixed;left:50%;bottom:0;transform:translateX(-50%);width:min(480px,100%);background:var(--card);border:1px solid var(--line);border-bottom:0;border-radius:18px 18px 0 0;padding:14px 14px calc(14px + env(safe-area-inset-bottom));z-index:6;box-shadow:0 -20px 60px rgba(0,0,0,.6)}
#tapePick h4{font:600 10px var(--font);letter-spacing:.2em;color:var(--dim);margin:0 0 10px}
#tapePick button{display:block;width:100%;text-align:left;padding:10px 12px;border-radius:10px;border:1px solid var(--line);margin-bottom:6px}
#tapePick button:hover{background:#1a1a20}
#tapePick input{width:100%;background:#0a0a0d;border:1px solid var(--line);border-radius:10px;padding:10px 12px;outline:none;margin-bottom:6px}
#tape{position:fixed;inset:0;background:var(--bg);padding:18px 16px calc(18px + env(safe-area-inset-bottom));overflow:auto;z-index:4}
#tape header{display:flex;justify-content:space-between;align-items:center;font:600 12px var(--font);letter-spacing:.2em;color:var(--dim);margin-bottom:12px;gap:10px}
#tape header input{flex:1;background:transparent;border:0;border-bottom:1px solid var(--line);font:600 18px var(--sans);letter-spacing:0;color:var(--ink);outline:none;padding:4px 0;min-width:0}
#tape .acts{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;justify-content:center}
#tapeCover{display:flex;justify-content:center;margin:0 0 16px}
#tapeCover canvas{width:min(50vw,220px);height:min(50vw,220px);border-radius:18px;box-shadow:0 30px 60px -30px rgba(0,0,0,.8)}
#tape .acts button{padding:9px 16px;border-radius:999px;border:1px solid var(--line);font:600 12px var(--font);letter-spacing:.14em;background:var(--card)}
#tape .acts button.go{background:var(--acc);color:var(--onacc);border-color:var(--acc)}
#tape .list{display:flex;flex-direction:column;gap:8px;max-width:640px;margin:0 auto}
#tape .row{display:flex;align-items:center;gap:12px;padding:8px;border-radius:14px;background:var(--card);border:1px solid var(--line)}
#tape .row.on{border-color:var(--acc)}
#tape .row canvas{width:52px;height:52px;border-radius:10px;flex:0 0 52px;cursor:pointer}
#tape .row .m{flex:1;min-width:0;cursor:pointer}
#tape .row b{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#tape .row small{color:var(--dim);font-size:11px;font-family:var(--font)}
#tape .row .ctl{display:flex;gap:2px}
#tape .row .ctl button{width:32px;height:32px;border-radius:8px;color:var(--dim);font-size:14px}
#tape .row .ctl button:hover{background:#1a1a20;color:var(--ink)}
#tape .empty{color:var(--dim);text-align:center;padding:40px 0}
/* ---------- library ---------- */
#lib{position:fixed;inset:0;background:var(--bg);padding:calc(18px + env(safe-area-inset-top)) 16px calc(90px + env(safe-area-inset-bottom));overflow:auto}
#lib .t.now .art{box-shadow:0 0 0 2px var(--ink)}
#lib .t.now .art::after{content:"▶";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:34px;height:34px;border-radius:50%;background:var(--ink);color:var(--bg);display:grid;place-items:center;font-size:13px;padding-left:2px}
#lib .t.now.paused .art::after{content:"‖";padding-left:0}
#libNow{position:fixed;left:50%;bottom:calc(14px + env(safe-area-inset-bottom));transform:translateX(-50%);width:min(560px,calc(100% - 24px));display:flex;align-items:center;gap:10px;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:8px 8px 8px 8px;z-index:4;box-shadow:0 20px 50px -20px rgba(0,0,0,.6)}
#libNow canvas{width:44px;height:44px;border-radius:9px;flex:none}
#libNow .t{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
#libNow b{font:600 13px var(--sans);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#libNow small{font:11px var(--font);letter-spacing:.14em;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#libNow button{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;color:var(--ink)}
#libNow button.go{background:var(--ink);color:var(--bg)}
#libNow button.open{width:auto;border-radius:999px;padding:0 12px;font:600 11px var(--font);letter-spacing:.14em;border:1px solid var(--line)}
#lib header{display:flex;justify-content:space-between;align-items:center;font:600 12px var(--font);letter-spacing:.2em;color:var(--dim);margin-bottom:16px}
#lib .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:14px}
#lib h3{font:600 11px var(--font);letter-spacing:.2em;color:var(--dim);margin:22px 0 10px;display:flex;align-items:center;gap:10px}
#lib h3 canvas{width:22px;height:22px;border-radius:50%}
#lib h3 small{font-weight:400;letter-spacing:.08em;opacity:.7}
#lib .t.on canvas{outline:2px solid var(--acc);outline-offset:2px}
#lib .t .art{position:relative;aspect-ratio:1;border-radius:12px;overflow:hidden;background:var(--card)}
#lib .t .art canvas{width:100%;height:100%;display:block}
#lib .t .kept{position:absolute;top:6px;right:6px;font:600 9px var(--font);letter-spacing:.18em;color:var(--onacc);background:var(--acc);border-radius:999px;padding:3px 7px}
#lib .t .re{position:absolute;left:6px;bottom:6px;font-size:13px;letter-spacing:1px;background:var(--veil2);border-radius:8px;padding:2px 6px;max-width:calc(100% - 12px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#lib .t small{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#lib .t{cursor:pointer}
#lib .t canvas{width:100%;aspect-ratio:1;border-radius:12px;display:block;background:var(--card)}
#lib .t b{display:block;font-size:12px;margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#lib .t small{color:var(--dim);font-size:11px}
#lib .empty{color:var(--dim);text-align:center;padding:40px 0}
#lib .cost{color:var(--dim);font:11px var(--font);margin-top:24px;text-align:center}
</style>
</head>
<body>
<!-- ============ screen 1: the seed ============ -->
<section id="seed">
<div class="wordmark">g<span class="ast">*</span>ndora</div>
<div id="nowRow" class="hidden"><div id="now" title="back to the station"><canvas width="56" height="56"></canvas><span id="nowTitle"></span><b>▶ PLAYING</b></div></div>
<form id="seedForm" autocomplete="off">
<div id="seedCover"><canvas width="240" height="240"></canvas></div>
<input id="seedIn" placeholder="*" maxlength="200" autofocus>
<div class="hint" id="seedHint">a mood, a line, a place, a word</div>
</form>
<div id="stations" class="hidden"><h3>YOUR STATIONS</h3><div class="row" id="stationRow"></div></div>
<div id="keyRow"><button class="nav" id="libBtn2" type="button">LIBRARY</button><button class="nav" id="tasteBtn2" type="button">TASTE</button><button class="nav theme" type="button" title="light / dark">LIGHT</button><button class="nav reload" type="button" title="reload the app">RELOAD</button><button id="keyBtn" type="button" class="hidden">shared budget · add a venice key</button></div>
<div id="keyBox" class="hidden">
<p>Optional. Without a key, gandora runs on a shared budget through inference.yolopreneur.lol (a few tracks a day per person). With your own Venice key it goes straight to api.venice.ai from this browser and the key goes nowhere else.</p>
<input id="keyIn" placeholder="sk-…" spellcheck="false">
<label>DJ model <input id="djIn" placeholder="claude-haiku-5-5" spellcheck="false"></label>
<div class="row"><button id="keyCancel" type="button">cancel</button><button id="keySave" class="go" type="button">save</button></div>
</div>
</section>
<!-- ============ screen 2: the station ============ -->
<section id="player" class="hidden">
<div id="top">
<button id="back" title="new seed">◂ SEED</button>
<div><button id="tasteBtn">TASTE</button> &nbsp; <button id="libBtn">LIBRARY</button> &nbsp; <button class="theme" title="light / dark">LIGHT</button></div>
</div>
<div id="stage">
<div class="station" id="stationName" title="rename, look, reset or delete"></div>
<div id="cover"><div class="card">
<div class="face"><canvas width="760" height="760"></canvas><div class="veil">COOKING</div></div>
<div class="face back"><div class="backTitle">LYRICS</div><pre id="lyrics"></pre></div>
</div><div id="trail" title="what this station has played · tap to jump"></div></div>
<div id="meta"><h1 id="title">…</h1></div>
<div id="bar" title="scrub"><i></i><b></b></div>
<div id="meta2"><p id="lyric" title="reply to the DJ"></p><div id="next"></div><div id="pills"><button id="lyricsBtn">⇄ LYRICS</button><button id="keep" disabled>➕ KEEP</button><button id="share" disabled>↗ SHARE</button></div><p id="tags" class="hidden"></p><div id="tagRow"></div><div id="tagList"></div></div>
<div id="controls">
<button id="backTrack" title="previous track">
<svg viewBox="0 0 24 24"><path d="M18 6l-8.5 6 8.5 6V6zM6 6v12h2V6H6z"/></svg>
</button>
<button id="shuffle" title="shuffle: mix the station's own library in">
<svg viewBox="0 0 24 24"><path d="M17 4h4v4l-1.5-1.5L16 10l-1.4-1.4 3.5-3.5L17 4zM3 6h4.5l3 3-1.4 1.4L7 8H3V6zm14.6 10.6 1.5-1.5v4h-4l1.5-1.5-3.1-3.1 1.4-1.4 2.7 2.7zM3 18v-2h4l9.5-9.5L18 8l-10 10H3z"/></svg>
</button>
<button id="prev" title="replay from the start">
<svg viewBox="0 0 24 24"><path d="M12 5V2L7 7l5 5V8c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6H4c0 4.4 3.6 8 8 8s8-3.6 8-8-3.6-8-8-8z"/></svg>
</button>
<button id="play" title="play / pause"><svg id="playIco" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></button>
<button id="loop" title="repeat this track">
<svg viewBox="0 0 24 24"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>
</button>
<button id="remix" title="remix this track: same bones, your changes">
<svg viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9h-2a7 7 0 1 1-7-7v3l4-4-4-4v3zm0 5a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 2a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></svg>
</button>
<button id="skip" title="next">
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zm10-12v12h2V6h-2z"/></svg>
</button>
</div>
</div>
<div id="react">
<div id="emojis">
<button data-e="🔥" title="more of this">🔥</button>
<button data-e="❤️" title="love it">❤️</button>
<button data-e="😴" title="too sleepy">😴</button>
<button data-e="🌧️" title="sadder">🌧️</button>
<button data-e="⚡" title="faster, louder">⚡</button>
<button data-e="🎸" title="more guitar">🎸</button>
<button data-e="🚫" title="not this">🚫</button>
<button id="more" title="more emoji">+</button>
</div>
<div id="picker" class="hidden"></div>
<form id="sayRow"><input id="say" placeholder="or say it: less snare, sadder but faster…" maxlength="240"><button>send</button></form>
</div>
<div id="toast"></div>
</section>
<section id="taste" class="hidden">
<header><span>TASTE</span><button id="tasteClose">CLOSE</button></header>
<div id="tasteTabs"></div>
<div id="tastePalette"></div>
<canvas id="brainMap" width="880" height="400"></canvas>
<canvas id="tasteMap" width="900" height="900"></canvas>
<div id="tasteLegend"><span>← slow · tempo · fast →</span><span>↑ energy</span><span>colour = key</span><span>ring = 🔥❤️ warm · 🚫⏭ cold</span></div>
<div id="tasteNote"></div>
</section>
<section id="tape" class="hidden">
<header><span>TAPE</span><input id="tapeName" maxlength="60"><button id="tapeClose">CLOSE</button></header>
<div id="tapeCover"><canvas width="480" height="480"></canvas></div>
<div class="acts"><button class="go" id="tapePlay">▶ PLAY ALL</button><button id="tapeLoop">🔁 REPEAT ALL</button><button id="tapeShare">↗ SHARE</button><button id="tapeMash" title="one new track out of up to three of these">⚡ MASHUP</button><button id="tapeDelete">DELETE TAPE</button></div>
<div class="list" id="tapeList"></div>
<div class="empty hidden" id="tapeEmpty">Empty tape. Add tracks from the library with the + on a tile.</div>
</section>
<div id="tapePick" class="hidden"></div>
<button id="upd" class="hidden" type="button">NEW VERSION · RELOAD</button>
<div id="stationSheet" class="hidden"></div>
<div id="scrim" class="hidden"></div>
<section id="lib" class="hidden">
<header><span>LIBRARY</span><button id="libClose">CLOSE</button></header>
<div class="grid" id="libGrid"></div>
<div class="empty hidden" id="libEmpty">Nothing played yet. Every track lands here; KEEP one to pin it for good.</div>
<div class="cost" id="libCost"></div>
<div class="cost"><a href="stats.html" title="your numbers" style="text-decoration:none;font-size:16px;opacity:.6;margin-right:10px">📈</a><a id="llama" href="webamp.html" title="it really whips" style="text-decoration:none;font-size:18px;opacity:.6">🦙</a></div>
<div id="libNow" class="hidden"><canvas width="120" height="120"></canvas><div class="t"><b></b><small></small></div><button class="go" id="libPlay" title="play / pause"><svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:currentColor"><path d="M8 5v14l11-7z"/></svg></button><button class="open" id="libOpen" title="back to the player">PLAYER ▸</button></div>
</section>
<script type="module" src="app.js"></script>
<script src="brain.js" defer></script>
</body>
</html>