Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
406 lines
34 KiB
HTML
406 lines
34 KiB
HTML
<!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> <button id="libBtn">LIBRARY</button> <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>
|