<!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover"><style>:root{color-scheme:light;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}html{scroll-padding-top:env(safe-area-inset-top,0px)}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#fff;color:#000}img{max-width:100%}[hidden]:not([hidden=until-found i]){display:none!important}</style></head><body>
<title>Workout Adventure</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=Big+Shoulders+Display:wght@700;800&family=IBM+Plex+Mono:wght@500;600&family=Source+Serif+4:wght@400;600&display=swap" rel="stylesheet">
<style>
/* Layout: one left-aligned column, gamebook pacing. Big condensed gym-signage names, serif cues, mono numbers. */
:root{
--bg:#EDF0F2; --card:#FFFFFF; --fg:#121A21; --muted:#56626D; --line:#C6CED5;
--strength:#1F4FD8; --pitfall:#C42F1B; --reward:#1B7447;
--btn:#121A21; --btn-fg:#FFFFFF;
--display:'Big Shoulders Display','Arial Narrow',Impact,sans-serif;
--body:'Source Serif 4',Georgia,serif;
--mono:'IBM Plex Mono',ui-monospace,Menlo,monospace;
}
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]){
--bg:#0E151B; --card:#16202A; --fg:#E9EEF2; --muted:#9AA7B3; --line:#2A3846;
--strength:#86A8FF; --pitfall:#FF8D78; --reward:#6FD5A0;
--btn:#E9EEF2; --btn-fg:#0E151B; color-scheme:dark;
}
}
:root[data-theme="dark"]{
--bg:#0E151B; --card:#16202A; --fg:#E9EEF2; --muted:#9AA7B3; --line:#2A3846;
--strength:#86A8FF; --pitfall:#FF8D78; --reward:#6FD5A0;
--btn:#E9EEF2; --btn-fg:#0E151B; color-scheme:dark;
}
*{box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:var(--body);font-size:19px;line-height:1.5;padding-inline:16px;padding-block:20px 56px}
.wrap{max-width:640px;margin-inline:auto;display:flex;flex-direction:column;gap:28px}
.top{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.brand{font-family:var(--mono);font-weight:600;font-size:13px;letter-spacing:.14em;text-transform:uppercase}
.trail{display:flex;gap:5px}
.trail i{width:14px;height:14px;border:2px solid var(--line);display:block}
.trail i.s{background:var(--strength);border-color:var(--strength)}
.trail i.c{background:var(--pitfall);border-color:var(--pitfall)}
.trail i.r{background:var(--reward);border-color:var(--reward)}
.trail i.now{border-color:var(--fg);outline:2px solid var(--fg);outline-offset:1px}
.label{font-family:var(--mono);font-weight:600;font-size:13px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.tag{display:inline-flex;align-items:center;gap:7px}
.tag::before{content:"";width:12px;height:12px;background:currentColor;display:block}
.tag.s{color:var(--strength)} .tag.c{color:var(--pitfall)} .tag.r{color:var(--reward)}
h1,h2{font-family:var(--display);font-weight:800;text-transform:uppercase;line-height:.92;letter-spacing:.005em;margin:0;text-wrap:balance;outline:none}
h1{font-size:clamp(52px,15vw,92px)}
h2{font-size:clamp(34px,9vw,48px)}
.rx{font-family:var(--mono);font-weight:600;font-size:clamp(24px,6vw,32px);margin:0}
.cue{margin:0;max-width:34em}
.stack{display:flex;flex-direction:column;gap:16px}
.note{margin:0;padding:12px 14px;border:2px solid currentColor;font-size:17px}
.note.c{color:var(--pitfall)} .note.r{color:var(--reward)}
.note b{font-family:var(--mono);font-size:13px;letter-spacing:.12em;text-transform:uppercase;display:block;margin-bottom:2px}
button{font:inherit;color:inherit;cursor:pointer}
.btn{background:var(--btn);color:var(--btn-fg);border:2px solid var(--btn);padding:16px 20px;font-family:var(--display);font-weight:800;text-transform:uppercase;font-size:26px;letter-spacing:.03em;text-align:left;width:100%;min-height:60px}
.btn.alt{background:transparent;color:var(--fg);border-color:var(--fg)}
.btn:focus-visible,.pick:focus-visible,.link:focus-visible{outline:3px solid var(--strength);outline-offset:3px}
.btn[disabled]{opacity:.5;cursor:default}
.timer{font-variant-numeric:tabular-nums}
.link{background:none;border:0;padding:6px 0;text-decoration:underline;text-underline-offset:4px;font-size:17px;color:var(--muted);align-self:flex-start}
.picks{display:flex;flex-direction:column;gap:12px}
.pick{display:grid;grid-template-columns:1fr auto;gap:2px 16px;align-items:baseline;text-align:left;background:var(--card);border:2px solid var(--fg);padding:14px 16px;min-height:76px;width:100%}
.pick:hover{border-color:var(--strength)}
.pick .name{font-family:var(--display);font-weight:800;text-transform:uppercase;font-size:32px;line-height:1;grid-column:1}
.pick .rxs{font-family:var(--mono);font-weight:600;font-size:15px;grid-column:2;grid-row:1;text-align:right;min-width:0}
.pick .label{grid-column:1 / -1;font-size:12px}
.legend{display:grid;gap:10px;margin:0;padding:0;list-style:none}
.legend li{display:grid;grid-template-columns:auto 1fr;gap:4px 12px;align-items:baseline}
.legend .tag{font-family:var(--mono);font-weight:600;font-size:13px;letter-spacing:.12em;text-transform:uppercase;min-width:120px}
.facts{display:flex;gap:10px 28px;flex-wrap:wrap;font-family:var(--mono);font-weight:600;font-size:15px;margin:0}
.facts span{white-space:nowrap}
.small{font-size:16px;color:var(--muted);margin:0}
.counts{display:flex;gap:10px 28px;flex-wrap:wrap}
.counts div{display:flex;flex-direction:column;gap:2px}
.counts strong{font-family:var(--display);font-weight:800;font-size:56px;line-height:.9}
.route{margin:0;padding-left:1.7em;display:flex;flex-direction:column;gap:2px}
.route li::marker{font-family:var(--mono);font-weight:600;font-size:15px;color:var(--muted)}
.route .tag{margin-right:8px}
.route .tag::before{width:10px;height:10px}
.route span.n{font-weight:600}
.route .rxs{font-family:var(--mono);font-size:14px;color:var(--muted);margin-left:8px}
hr{border:0;border-top:2px solid var(--line);margin:0}
@media (max-width:420px){ .pick .name{font-size:28px} .btn{font-size:23px} }
@media (prefers-reduced-motion:no-preference){ .pick,.btn{transition:border-color .12s,transform .12s} .pick:active,.btn:active{transform:translateY(1px)} }
</style>
<div class="wrap" id="app"></div>
<script>
/*DATA*/
// ADVENTURE DATA. To make a new adventure, copy this block and change the moves.
// t: S strength, C cardio burst (pitfall), R stretch (reward), E ending
// p: what to do, s: timer seconds (optional), cue: coaching line, go: the 2-3 moves you can pick next
const ADVENTURE = {
title: 'The Living Room Loop',
start: 'a1',
nodes: {
a1:{t:'S',n:'Bodyweight Squat',p:'10 reps',cue:'Sit back like a chair is behind you. Knees stay over your toes.',go:['b1','b2','b3']},
b1:{t:'S',n:'Push-Up',p:'8 reps',cue:'Hands on a counter, or knees on the floor. Keep your body in one line.',go:['c1','c3','c4']},
b2:{t:'S',n:'Reverse Lunge',p:'6 each leg',cue:'Step back, lower with control, push through the front heel.',go:['c2','c3','c4']},
b3:{t:'C',n:'Burpee Burst',p:'30 seconds',s:30,cue:'Squat down, step your feet back, step up, reach high. Slow steps still count.',go:['c2','c3','c4']},
c1:{t:'S',n:'Plank',p:'30 seconds',s:30,cue:'Forearms down, ribs in, keep breathing. Drop to your knees if you need to.',go:['d1','d2','d4']},
c2:{t:'S',n:'Glute Bridge',p:'12 reps',cue:'Feet flat. Squeeze at the top and pause for one count.',go:['d1','d3','d4']},
c3:{t:'C',n:'Mountain Climbers',p:'30 seconds',s:30,cue:'Hands on a counter makes this easier. Drive one knee at a time.',go:['d1','d3','d4']},
c4:{t:'R',n:'Doorway Chest Opener',p:'30 seconds',s:30,cue:'Forearm on the door frame. Step through gently until you feel a stretch, not a pinch.',go:['d1','d2','d3']},
d1:{t:'S',n:'Wall Sit',p:'30 seconds',s:30,cue:'Back flat on the wall, thighs toward level. Breathe slowly.',go:['e1','e2','e3']},
d2:{t:'S',n:'Bent-Over Row',p:'10 reps',cue:'Hinge at the hips. Pull a backpack or two water jugs toward your ribs.',go:['e1','e2','e3']},
d3:{t:'R',n:'Hip Flexor Stretch',p:'30 seconds each side',s:30,cue:'Half-kneel, tuck your tailbone, shift forward until the front of the hip opens.',go:['e1','e2','e3']},
d4:{t:'C',n:'High Knees',p:'30 seconds',s:30,cue:'March fast and lift your knees as high as feels good. Low impact is fine.',go:['e1','e3']},
e1:{t:'S',n:'Step-Up',p:'8 each leg',cue:'Use a sturdy stair. Drive through the heel of the top foot.',go:['f1','f2','f3']},
e2:{t:'S',n:'Dead Bug',p:'8 each side',cue:'On your back, lower the opposite arm and leg slowly. Low back stays flat.',go:['f1','f2','f3']},
e3:{t:'R',n:"Child's Pose",p:'45 seconds',s:45,cue:'Hips back toward your heels, arms long. Let the breath get slow.',go:['f1','f3']},
f1:{t:'S',n:'Calf Raise',p:'15 reps',cue:'Hold a counter. Rise tall, lower slowly.',go:['g1','g2','g3']},
f2:{t:'C',n:'Jumping Jacks',p:'30 seconds',s:30,cue:'Step-jacks work too. Keep it smooth and keep moving.',go:['g1','g2']},
f3:{t:'R',n:'Cat-Cow',p:'8 rounds',cue:'Hands and knees. Round your back, then arch, nice and slow.',go:['g1','g2','g3']},
g1:{t:'S',n:'Chair Dip',p:'8 reps',cue:'Use a sturdy chair against a wall. Bend your elbows straight back.',go:['h1','h2']},
g2:{t:'S',n:'Side Plank',p:'20 seconds each side',s:20,cue:'Elbow under shoulder. Knees down is a good version.',go:['h1','h2']},
g3:{t:'C',n:'Skater Steps',p:'30 seconds',s:30,cue:'Step side to side with soft landings. Keep the steps small.',go:['h1','h2']},
h1:{t:'S',n:"Farmer's March",p:'40 steps',cue:'Carry something heavy in each hand. Walk tall and keep your shoulders down.',go:['z1','z2']},
h2:{t:'R',n:'Hamstring Stretch',p:'30 seconds each side',s:30,cue:'Heel forward, hinge at the hips, long back.',go:['z2','z3']},
z1:{t:'E',n:'Strong Finish',txt:'You picked the hard road and kept going. Shake out your arms and take a drink of water.'},
z2:{t:'E',n:'Easy Landing',txt:'You came down slow with an easy breath. Good pacing is a skill.'},
z3:{t:'E',n:'Heart Pounder',txt:'You went out with a burst. Walk it around the room for a minute before you sit.'}
}
};
/*END*/
const N = ADVENTURE.nodes;
const KIND = {S:{cls:'s',label:'Strength'},C:{cls:'c',label:'Pitfall'},R:{cls:'r',label:'Reward'}};
const app = document.getElementById('app');
const state = {screen:'start', path:[], ready:false, left:null};
let tick = null;
function depth(id, memo={}){
const nd = N[id];
if (nd.t==='E') return 0;
if (memo[id]!==undefined) return memo[id];
return memo[id] = 1 + Math.max(...nd.go.map(g=>depth(g,memo)));
}
const TOTAL = depth(ADVENTURE.start);
const ENDS = Object.keys(N).filter(k=>N[k].t==='E');
function store(get, val){
try{
const k='workout-adventure-endings';
const cur = JSON.parse(localStorage.getItem(k)||'[]');
if (get) return cur;
if (!cur.includes(val)){ cur.push(val); localStorage.setItem(k, JSON.stringify(cur)); }
return cur;
}catch(e){ return val ? [val] : []; }
}
function trail(){
let h='';
for (let i=0;i<TOTAL;i++){
const id = state.path[i];
const cls = id ? KIND[N[id].t].cls : '';
const now = (i===state.path.length-1 && state.screen==='play') ? ' now' : '';
h += `<i class="${cls}${now}"></i>`;
}
return `<div class="trail" role="img" aria-label="Moves so far: ${state.path.length} of ${TOTAL}">${h}</div>`;
}
function top(){
return `<div class="top"><span class="brand">Justin's Fitness</span>${state.screen==='start'?'':trail()}</div>`;
}
function tag(t){ const k=KIND[t]; return `<span class="tag ${k.cls}">${k.label}</span>`; }
function startView(){
return `${top()}
<div class="stack">
<div class="label">Adventure 1</div>
<h1 tabindex="-1">${ADVENTURE.title}</h1>
<p class="cue">Pick a move and your pick decides what comes next. You will land on strength work, cardio pitfalls and stretching rewards. Every route is different.</p>
</div>
<ul class="legend">
<li>${tag('S')}<span>Builds the path. Reps and sets.</span></li>
<li>${tag('C')}<span>A short burst. Keep it smooth.</span></li>
<li>${tag('R')}<span>A stretch. Breathe and reset.</span></li>
</ul>
<p class="facts"><span>${TOTAL} moves</span><span>About ${Math.round(TOTAL*55/60)} min</span><span>No equipment</span></p>
<div class="stack">
<button class="btn" data-act="begin">Begin</button>
<p class="small">A sturdy chair, a counter and a towel or backpack help. Move at your own pace and stop if anything hurts. Check with your doctor first if you are unsure.</p>
<p class="small">Endings found: ${store(true).length} of ${ENDS.length}</p>
</div>`;
}
function playView(){
const id = state.path[state.path.length-1];
const nd = N[id];
const k = KIND[nd.t];
const banner = nd.t==='C' ? `<p class="note c"><b>Pitfall</b>Keep it smooth and keep moving.</p>`
: nd.t==='R' ? `<p class="note r"><b>Reward</b>Breathe and let it land.</p>` : '';
let timer='';
if (nd.s && !state.ready){
timer = `<button class="btn alt" data-act="timer" ${state.left!==null?'disabled':''}><span class="timer" id="timer">${state.left!==null?state.left+' s':'Start '+nd.s+' second timer'}</span></button>`;
}
let lower='';
if (!state.ready){
lower = `${timer}<button class="btn" data-act="done">Done. Show my choices</button>`;
} else {
lower = `<hr><div class="stack"><h2>Pick your next move</h2>
<div class="picks">${nd.go.map(g=>{
const x=N[g]; const kk=KIND[x.t];
return `<button class="pick" data-act="pick" data-id="${g}"><span class="name">${x.n}</span><span class="rxs">${x.p||''}</span><span class="label">${tag(x.t)}</span></button>`;
}).join('')}</div></div>`;
}
const undo = state.path.length>1 ? `<button class="link" data-act="undo">Undo last pick</button>` : '';
return `${top()}
<div class="stack">
<div class="label"><span>Move ${state.path.length} of ${TOTAL}</span>${tag(nd.t)}</div>
<h1 tabindex="-1">${nd.n}</h1>
<p class="rx">${nd.p}</p>
<p class="cue">${nd.cue}</p>
${banner}
</div>
<div class="stack">${lower}${undo}</div>`;
}
function endView(){
const id = state.path[state.path.length-1];
const nd = N[id];
const moves = state.path.slice(0,-1);
const c = {S:0,C:0,R:0};
moves.forEach(m=>c[N[m].t]++);
const found = store(true);
const rows = moves.map(m=>{
const x=N[m]; return `<li>${tag(x.t)}<span class="n">${x.n}</span><span class="rxs">${x.p}</span></li>`;
}).join('');
return `${top()}
<div class="stack">
<div class="label">The end</div>
<h1 tabindex="-1">${nd.n}</h1>
<p class="cue">${nd.txt}</p>
</div>
<div class="counts">
<div><strong>${c.S}</strong>${tag('S')}</div>
<div><strong>${c.C}</strong>${tag('C')}</div>
<div><strong>${c.R}</strong>${tag('R')}</div>
</div>
<div class="stack">
<div class="label">Your route</div>
<ol class="route">${rows}</ol>
</div>
<hr>
<div class="stack">
<button class="btn" data-act="restart">Try another route</button>
<p class="small">Endings found: ${found.length} of ${ENDS.length}</p>
</div>`;
}
function render(focus){
clearInterval(tick); tick=null;
app.innerHTML = state.screen==='start' ? startView() : state.screen==='end' ? endView() : playView();
if (focus){ window.scrollTo(0,0); const h=app.querySelector('h1'); if(h) h.focus({preventScroll:true}); }
}
function startTimer(){
const nd = N[state.path[state.path.length-1]];
state.left = nd.s;
render(false);
tick = setInterval(()=>{
state.left--;
const el = document.getElementById('timer');
if (state.left<=0){
clearInterval(tick); tick=null;
state.left=null; state.ready=true; render(false);
const first = app.querySelector('.pick'); if(first) first.scrollIntoView({block:'nearest'});
return;
}
if (el) el.textContent = state.left+' s';
},1000);
}
app.addEventListener('click', e=>{
const b = e.target.closest('[data-act]'); if(!b) return;
const act = b.dataset.act;
if (act==='begin'){ state.screen='play'; state.path=[ADVENTURE.start]; state.ready=false; state.left=null; render(true); }
else if (act==='timer'){ startTimer(); }
else if (act==='done'){ state.ready=true; state.left=null; render(false); const p=app.querySelector('.picks'); if(p) p.scrollIntoView({block:'nearest',behavior:'smooth'}); }
else if (act==='pick'){
const id=b.dataset.id; state.path.push(id); state.ready=false; state.left=null;
if (N[id].t==='E'){ state.screen='end'; store(false,id); }
render(true);
}
else if (act==='undo'){ state.path.pop(); state.ready=true; state.left=null; render(true); }
else if (act==='restart'){ state.screen='start'; state.path=[]; state.ready=false; render(true); }
});
render(false);
</script>
</body></html>