Workout Adventure
Workout Adventure
Workout Adventure
Workout Adventure
<!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>