← back to Model Arena

data/artifacts/035b7009e366/claude-code.html

654 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Designer Wallcoverings — Pattern Runner</title>
<style>
  :root{
    /* ——— luxe-noir design tokens ——— */
    --bg:#0d0d0f;
    --surface:#17171c;
    --surface-2:#1e1e25;
    --ink:#f5f2ea;
    --muted:#8a8578;
    --accent:#c9a961;
    --accent-deep:#7d6a45;
    --line:rgba(201,169,97,.28);
    --hairline:rgba(245,242,234,.10);
    --shadow:0 16px 48px rgba(0,0,0,.55);
    --glow:0 0 28px rgba(201,169,97,.35);
    --ease:cubic-bezier(.22,1,.36,1);
    --serif:'Didot','Bodoni MT','Playfair Display',Georgia,'Times New Roman',serif;
    --sans:-apple-system,BlinkMacSystemFont,'Segoe UI','Helvetica Neue',Arial,sans-serif;
  }

  *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
  html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--ink);font-family:var(--sans);}

  /* ——— fixed luxury top bar ——— */
  .topbar{
    position:fixed;top:0;left:0;right:0;z-index:20;
    height:84px;
    display:flex;align-items:center;justify-content:space-between;
    padding:0 clamp(18px,4vw,48px);
    background:linear-gradient(180deg,rgba(13,13,15,.96) 0%,rgba(13,13,15,.72) 70%,rgba(13,13,15,0) 100%);
    border-bottom:1px solid var(--hairline);
    pointer-events:none;
  }
  .wordmark{
    font-family:var(--serif);
    text-transform:uppercase;
    letter-spacing:.18em;
    color:var(--ink);
    line-height:1.02;
    font-weight:500;
    text-shadow:0 1px 0 rgba(0,0,0,.4);
  }
  .wordmark .l1{display:block;font-size:26px;}
  .wordmark .l2{display:block;font-size:20px;color:var(--accent);letter-spacing:.205em;}

  /* scoreboard — demoted refined sans */
  .scores{display:flex;gap:clamp(14px,3vw,34px);align-items:flex-end;}
  .score-item{text-align:right;}
  .score-item .lab{
    font-family:var(--sans);
    font-size:11px;text-transform:uppercase;letter-spacing:.22em;
    color:var(--muted);display:block;margin-bottom:3px;
  }
  .score-item .val{
    font-family:var(--serif);
    font-size:30px;letter-spacing:.04em;color:var(--ink);
    font-variant-numeric:tabular-nums;font-weight:500;
  }
  .score-item.best .val{color:var(--accent);}

  /* ——— canvas ——— */
  #game{display:block;position:absolute;inset:0;width:100%;height:100%;}

  /* ——— overlays ——— */
  .overlay{
    position:fixed;inset:0;z-index:30;
    display:flex;align-items:center;justify-content:center;
    background:radial-gradient(120% 80% at 50% 40%,rgba(23,23,28,.72),rgba(13,13,15,.94));
    backdrop-filter:blur(3px);
    padding:24px;
    transition:opacity .5s var(--ease);
  }
  .overlay.hidden{opacity:0;pointer-events:none;}
  .card{
    width:min(560px,92vw);
    background:linear-gradient(180deg,var(--surface),var(--surface-2));
    border:1px solid var(--line);
    border-radius:14px;
    box-shadow:var(--shadow);
    padding:clamp(32px,5vw,56px);
    text-align:center;
    animation:fadeUp .7s var(--ease) both;
  }
  @keyframes fadeUp{from{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}
  .kicker{
    font-family:var(--sans);font-size:12px;text-transform:uppercase;
    letter-spacing:.28em;color:var(--accent);margin-bottom:20px;
  }
  .card h1{
    font-family:var(--serif);font-weight:500;
    font-size:clamp(38px,7vw,58px);line-height:1.02;letter-spacing:.01em;
    color:var(--ink);margin-bottom:8px;
  }
  .card h1 .accent{color:var(--accent);font-style:italic;}
  .card p{
    font-family:var(--sans);font-size:16px;line-height:1.7;
    color:var(--muted);max-width:40ch;margin:14px auto 4px;
  }
  .rule{width:56px;height:1px;background:var(--line);margin:26px auto;}

  .result-row{display:flex;justify-content:center;gap:48px;margin:26px 0 6px;}
  .result-row .lab{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted);display:block;margin-bottom:6px;}
  .result-row .num{font-family:var(--serif);font-size:44px;color:var(--ink);font-variant-numeric:tabular-nums;}
  .result-row .num.gold{color:var(--accent);}

  .btn{
    margin-top:30px;
    font-family:var(--sans);font-size:12.5px;text-transform:uppercase;letter-spacing:.24em;
    color:var(--bg);background:var(--accent);
    border:none;border-radius:999px;
    padding:16px 40px;cursor:pointer;
    transition:transform .2s var(--ease),box-shadow .3s var(--ease),background .3s var(--ease);
    box-shadow:var(--glow);
  }
  .btn:hover{transform:translateY(-2px);background:#d8ba72;box-shadow:0 0 34px rgba(201,169,97,.5);}
  .btn:active{transform:translateY(0);}

  /* legend of collectible styles */
  .legend{display:flex;flex-wrap:wrap;justify-content:center;gap:14px 22px;margin-top:30px;}
  .legend .sw{display:flex;align-items:center;gap:9px;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);}
  .legend .dot{width:18px;height:18px;border-radius:4px;border:1px solid var(--hairline);flex:none;box-shadow:inset 0 0 0 1px rgba(0,0,0,.25);}

  .hint{
    position:fixed;left:0;right:0;bottom:22px;z-index:15;text-align:center;
    font-family:var(--sans);font-size:11px;letter-spacing:.24em;text-transform:uppercase;
    color:var(--muted);pointer-events:none;transition:opacity .4s var(--ease);
  }
  .hint.hidden{opacity:0;}

  @media(max-width:520px){
    .topbar{height:72px;}
    .wordmark .l1{font-size:21px;}
    .wordmark .l2{font-size:16px;}
    .score-item .val{font-size:24px;}
  }
</style>
</head>
<body>

  <header class="topbar">
    <div class="wordmark" aria-label="Designer Wallcoverings">
      <span class="l1">Designer</span>
      <span class="l2">Wallcoverings</span>
    </div>
    <div class="scores">
      <div class="score-item"><span class="lab">Score</span><span class="val" id="scoreVal">0</span></div>
      <div class="score-item best"><span class="lab">Best</span><span class="val" id="bestVal">0</span></div>
    </div>
  </header>

  <canvas id="game"></canvas>

  <div class="hint" id="hint">Tap · Space to Jump</div>

  <!-- START -->
  <div class="overlay" id="startOverlay">
    <div class="card">
      <div class="kicker">The House Atelier</div>
      <h1>Pattern <span class="accent">Runner</span></h1>
      <p>Roll the wall. Gather rare wallpaper bolts, dodge the paint hazards, and hold your line as the atelier quickens.</p>
      <div class="legend" id="legendStart"></div>
      <div class="rule"></div>
      <button class="btn" id="startBtn">Begin the Run</button>
    </div>
  </div>

  <!-- GAME OVER -->
  <div class="overlay hidden" id="overOverlay">
    <div class="card">
      <div class="kicker" id="overKicker">The Run Concluded</div>
      <h1>Curtain <span class="accent">Call</span></h1>
      <div class="result-row">
        <div><span class="lab">Score</span><span class="num gold" id="finalScore">0</span></div>
        <div><span class="lab">Best</span><span class="num" id="finalBest">0</span></div>
      </div>
      <p id="overNote">A refined effort. The wall awaits your return.</p>
      <div class="rule"></div>
      <button class="btn" id="restartBtn">Run Again</button>
    </div>
  </div>

<script>
(function(){
  "use strict";

  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');

  // ——— design tokens mirrored for canvas ———
  const T = {
    bg:'#0d0d0f', surface:'#17171c', ink:'#f5f2ea', muted:'#8a8578',
    accent:'#c9a961', accentDeep:'#7d6a45'
  };

  // Wallpaper styles — visually distinct jewel motifs (color carries meaning)
  const STYLES = [
    { name:'Damask',    color:'#2f6b4f', accent:'#7bd6a2', motif:'damask' },
    { name:'Floral',    color:'#a63a5b', accent:'#f2a6bd', motif:'floral' },
    { name:'Geometric', color:'#33507e', accent:'#8fb3ea', motif:'geo'    },
    { name:'Stripe',    color:'#b3792a', accent:'#f0c98a', motif:'stripe' },
    { name:'Toile',     color:'#5e4785', accent:'#c3aef0', motif:'toile'  }
  ];

  // ——— sizing / DPR ———
  let W=0,H=0,DPR=1, ground=0;
  function resize(){
    DPR = Math.min(window.devicePixelRatio||1, 2);
    W = window.innerWidth; H = window.innerHeight;
    canvas.width = Math.floor(W*DPR); canvas.height = Math.floor(H*DPR);
    canvas.style.width = W+'px'; canvas.style.height = H+'px';
    ctx.setTransform(DPR,0,0,DPR,0,0);
    ground = H - Math.max(96, H*0.16);
  }
  window.addEventListener('resize', resize);

  // ——— game state ———
  const G = {
    state:'start',      // start | playing | over
    speed:0, baseSpeed:340, speedGrow:8,   // px/s
    score:0, best:0, dist:0,
    bgScroll:0, groundScroll:0,
    spawnCd:0, styleIdx:0,
    shake:0
  };
  G.best = parseInt(localStorage.getItem('dw_pattern_best')||'0',10)||0;

  // Player — paint roller
  const P = {
    x:0, y:0, w:64, h:44,
    vy:0, onGround:true, roll:0
  };
  const GRAV = 2400, JUMP = -880;

  function resetPlayer(){
    P.w = Math.max(52, Math.min(72, W*0.06));
    P.h = P.w*0.7;
    P.x = Math.max(70, W*0.16);
    P.y = ground - P.h;
    P.vy = 0; P.onGround = true; P.roll = 0;
  }

  let items = [];   // collectibles + obstacles
  let motes = [];   // ambient gold dust

  function startGame(){
    resize();
    G.state='playing'; G.speed=G.baseSpeed; G.score=0; G.dist=0;
    G.spawnCd=0.6; G.styleIdx=0; G.shake=0;
    items=[]; motes=[];
    for(let i=0;i<26;i++){
      motes.push({x:Math.random()*W,y:Math.random()*H,r:Math.random()*1.6+.4,s:Math.random()*10+4});
    }
    resetPlayer();
    updateScore();
    hide(startOverlay); hide(overOverlay);
    hint.classList.remove('hidden');
    setTimeout(()=>hint.classList.add('hidden'), 2600);
  }

  function endGame(){
    G.state='over';
    if(G.score > G.best){ G.best = G.score; localStorage.setItem('dw_pattern_best', String(G.best)); }
    document.getElementById('finalScore').textContent = G.score;
    document.getElementById('finalBest').textContent = G.best;
    updateScore();
    const note = document.getElementById('overNote');
    const kick = document.getElementById('overKicker');
    if(G.score>=G.best && G.score>0){ kick.textContent='A New House Record'; note.textContent='Exquisite. The finest run yet — the atelier is impressed.'; }
    else if(G.score>=30){ note.textContent='A refined effort. The wall awaits your return.'; }
    else { note.textContent='A promising start. Find your rhythm and roll on.'; }
    show(overOverlay);
  }

  function jump(){
    if(G.state!=='playing') return;
    if(P.onGround){ P.vy = JUMP; P.onGround=false; hint.classList.add('hidden'); }
  }

  // ——— spawning ———
  function spawn(){
    // 62% collectible, 38% obstacle
    const isItem = Math.random() < 0.62;
    if(isItem){
      const st = STYLES[(G.styleIdx++) % STYLES.length];
      const elevated = Math.random()<0.45;
      const size = 40;
      items.push({
        type:'roll', style:st, x:W+60,
        y: elevated ? ground - P.h - (60+Math.random()*70) : ground - size - 8,
        w:size, h:size*1.5, phase:Math.random()*6.28, taken:false
      });
    } else {
      const kinds=['bucket','ladder','sign'];
      const k=kinds[(Math.random()*kinds.length)|0];
      let w,h;
      if(k==='bucket'){ w=44; h=42; }
      else if(k==='ladder'){ w=40; h=Math.min(120, ground*0.22+40); }
      else { w=52; h=46; }
      items.push({ type:'obstacle', kind:k, x:W+60, y:ground-h, w:w, h:h });
    }
  }

  // ——— collision ———
  function hit(a,b, pad){
    pad = pad||0;
    return a.x < b.x+b.w-pad && a.x+a.w > b.x+pad &&
           a.y < b.y+b.h-pad && a.y+a.h > b.y+pad;
  }

  // ——— update ———
  let last=0;
  function frame(ts){
    if(!last) last=ts;
    let dt=(ts-last)/1000; last=ts;
    if(dt>0.05) dt=0.05; // clamp after tab-away

    if(G.state==='playing') update(dt);
    render(dt);
    requestAnimationFrame(frame);
  }

  function update(dt){
    G.speed += G.speedGrow*dt;
    G.dist += G.speed*dt;
    G.bgScroll = (G.bgScroll + G.speed*0.28*dt) % 240;
    G.groundScroll = (G.groundScroll + G.speed*dt) % 80;

    // player physics
    P.vy += GRAV*dt;
    P.y += P.vy*dt;
    if(P.y >= ground - P.h){ P.y = ground-P.h; P.vy=0; P.onGround=true; }
    P.roll += (G.speed/ (P.w*0.5)) * dt; // rolling spin

    // spawn cadence tied to speed
    G.spawnCd -= dt;
    if(G.spawnCd<=0){
      spawn();
      const gapBase = Math.max(0.62, 1.35 - G.dist/9000);
      G.spawnCd = gapBase + Math.random()*0.55;
    }

    // move + resolve items
    const px = {x:P.x+6, y:P.y+4, w:P.w-12, h:P.h-8};
    for(let i=items.length-1;i>=0;i--){
      const it=items[i];
      it.x -= G.speed*dt;
      if(it.type==='roll') it.phase += dt*2;

      if(it.type==='roll' && !it.taken && hit(px,it,4)){
        it.taken=true; it.pop=0; G.score += 1; updateScore(); G.shake=Math.min(G.shake+2,5);
      } else if(it.type==='obstacle' && hit(px,it,7)){
        G.shake=16; endGame(); return;
      }
      if(it.x + it.w < -80) items.splice(i,1);
      if(it.taken){ it.pop=(it.pop||0)+dt*4; if(it.pop>1) items.splice(i,1); }
    }

    // ambient motes
    for(const m of motes){ m.x -= (G.speed*0.12 + m.s)*dt; if(m.x<-4){ m.x=W+4; m.y=Math.random()*H; } }

    if(G.shake>0) G.shake=Math.max(0,G.shake-dt*22);
  }

  // ——— render ———
  function render(dt){
    ctx.clearRect(0,0,W,H);

    // shake
    let ox=0,oy=0;
    if(G.shake>0){ ox=(Math.random()-.5)*G.shake; oy=(Math.random()-.5)*G.shake; }
    ctx.save(); ctx.translate(ox,oy);

    drawBackground();
    drawGround();

    // ambient gold dust
    for(const m of motes){
      ctx.globalAlpha=0.22; ctx.fillStyle=T.accent;
      ctx.beginPath(); ctx.arc(m.x,m.y,m.r,0,6.28); ctx.fill();
    }
    ctx.globalAlpha=1;

    // items
    for(const it of items){
      if(it.type==='roll') drawRoll(it); else drawObstacle(it);
    }

    drawRoller();

    ctx.restore();
  }

  function drawBackground(){
    // deep vignette wall
    const g=ctx.createLinearGradient(0,0,0,H);
    g.addColorStop(0,'#141419'); g.addColorStop(0.62,'#0f0f13'); g.addColorStop(1,'#0b0b0e');
    ctx.fillStyle=g; ctx.fillRect(0,0,W,H);

    // subtle scrolling damask lattice on the wall (single accent, low opacity)
    ctx.save();
    ctx.globalAlpha=0.06; ctx.strokeStyle=T.accent; ctx.lineWidth=1;
    const s=120, off=-G.bgScroll;
    for(let x=off-s; x<W+s; x+=s){
      for(let y=-s; y<ground+s; y+=s){
        ctx.beginPath();
        ctx.ellipse(x+s/2, y+s/2, s*0.30, s*0.44, 0, 0, 6.28);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(x+s/2, y+s/2, 3, 0, 6.28); ctx.stroke();
      }
    }
    ctx.restore();

    // soft radial glow near player lane
    const rg=ctx.createRadialGradient(P.x+40, ground-60, 20, P.x+40, ground-60, 360);
    rg.addColorStop(0,'rgba(201,169,97,0.10)'); rg.addColorStop(1,'rgba(201,169,97,0)');
    ctx.fillStyle=rg; ctx.fillRect(0,0,W,H);
  }

  function drawGround(){
    // baseboard / floor
    ctx.fillStyle='#08080a';
    ctx.fillRect(0,ground,W,H-ground);
    // hairline accent rail
    ctx.strokeStyle='rgba(201,169,97,0.5)'; ctx.lineWidth=1;
    ctx.beginPath(); ctx.moveTo(0,ground+0.5); ctx.lineTo(W,ground+0.5); ctx.stroke();
    // molding shadow
    const mg=ctx.createLinearGradient(0,ground,0,ground+26);
    mg.addColorStop(0,'rgba(201,169,97,0.14)'); mg.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=mg; ctx.fillRect(0,ground+1,W,26);
    // scrolling parquet ticks
    ctx.strokeStyle='rgba(245,242,234,0.05)'; ctx.lineWidth=1;
    for(let x=-G.groundScroll; x<W; x+=80){
      ctx.beginPath(); ctx.moveTo(x, ground+8); ctx.lineTo(x, H); ctx.stroke();
    }
  }

  // Paint roller (player)
  function drawRoller(){
    const x=P.x, y=P.y, w=P.w, h=P.h;
    const cy=y+h*0.62, cx=x+w*0.5, r=Math.min(w,h)*0.42;

    // ground shadow
    const airborne = ground-P.h - y;
    const sc = Math.max(0.35, 1 - airborne/260);
    ctx.save();
    ctx.globalAlpha=0.38*sc; ctx.fillStyle='#000';
    ctx.beginPath(); ctx.ellipse(cx, ground-4, r*1.15*sc, 6*sc, 0, 0, 6.28); ctx.fill();
    ctx.restore();

    // handle
    ctx.strokeStyle=T.accent; ctx.lineWidth=4; ctx.lineCap='round';
    ctx.beginPath();
    ctx.moveTo(x+w*0.5, cy);
    ctx.lineTo(x+w*0.5, y-6);
    ctx.lineTo(x+w*0.86, y-16);
    ctx.stroke();
    ctx.lineWidth=3;
    ctx.beginPath(); ctx.moveTo(x+w*0.5, y-6); ctx.lineTo(x+w*0.72, y-11); ctx.stroke();

    // roller cylinder (spinning)
    ctx.save();
    ctx.translate(cx,cy);
    // body
    const bg=ctx.createLinearGradient(-r,-r,r,r);
    bg.addColorStop(0,'#e9e4d6'); bg.addColorStop(0.5,'#f5f2ea'); bg.addColorStop(1,'#cfc7b4');
    ctx.fillStyle=bg;
    ctx.beginPath(); ctx.arc(0,0,r,0,6.28); ctx.fill();
    // rim
    ctx.strokeStyle=T.accentDeep; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(0,0,r,0,6.28); ctx.stroke();
    // spinning nap marks
    ctx.rotate(P.roll);
    ctx.strokeStyle='rgba(125,106,69,0.55)'; ctx.lineWidth=2;
    for(let i=0;i<6;i++){
      const a=i*(6.28/6);
      ctx.beginPath();
      ctx.moveTo(Math.cos(a)*r*0.35, Math.sin(a)*r*0.35);
      ctx.lineTo(Math.cos(a)*r*0.9, Math.sin(a)*r*0.9);
      ctx.stroke();
    }
    // gold hub
    ctx.rotate(-P.roll);
    ctx.fillStyle=T.accent;
    ctx.beginPath(); ctx.arc(0,0,r*0.24,0,6.28); ctx.fill();
    ctx.restore();

    // fresh paint trail behind
    ctx.globalAlpha=0.5; ctx.strokeStyle=T.accent; ctx.lineWidth=3;
    ctx.beginPath(); ctx.moveTo(cx-r, cy+r*0.7); ctx.lineTo(cx-r-38, cy+r*0.7); ctx.stroke();
    ctx.globalAlpha=1;
  }

  // Wallpaper bolt collectible — distinct motif per style
  function drawRoll(it){
    const bob = Math.sin(it.phase)*4;
    const x=it.x, y=it.y+bob, w=it.w, h=it.h;
    const pop = it.taken ? (it.pop||0) : 0;

    ctx.save();
    ctx.translate(x+w/2, y+h/2);
    if(it.taken){ ctx.globalAlpha=Math.max(0,1-pop); ctx.scale(1+pop*0.7,1+pop*0.7); ctx.translate(0,-pop*30); }
    ctx.translate(-w/2,-h/2);

    // glow halo
    ctx.save();
    ctx.globalAlpha*=0.5;
    const hg=ctx.createRadialGradient(w/2,h/2,2,w/2,h/2,w*0.95);
    hg.addColorStop(0, it.style.accent+'55'); hg.addColorStop(1,'transparent');
    ctx.fillStyle=hg; ctx.fillRect(-w*0.4,-h*0.4,w*1.8,h*1.8);
    ctx.restore();

    // bolt body (rounded rect)
    roundRect(0,0,w,h,6);
    const bg=ctx.createLinearGradient(0,0,w,h);
    bg.addColorStop(0, shade(it.style.color,1.18)); bg.addColorStop(1, shade(it.style.color,0.82));
    ctx.fillStyle=bg; ctx.fill();
    ctx.strokeStyle='rgba(245,242,234,0.35)'; ctx.lineWidth=1; ctx.stroke();

    // rolled edge (right)
    ctx.fillStyle=shade(it.style.color,0.6);
    roundRect(w-8,0,8,h,3); ctx.fill();

    // motif
    ctx.save();
    ctx.beginPath(); roundRect(3,3,w-11,h-6,4); ctx.clip();
    drawMotif(it.style, w, h);
    ctx.restore();

    // gold band label (no letters — color carries meaning)
    ctx.fillStyle=T.accent;
    ctx.fillRect(0,h*0.5-3,w-6,6);

    ctx.restore();
  }

  function drawMotif(st, w, h){
    ctx.strokeStyle=st.accent; ctx.fillStyle=st.accent; ctx.lineWidth=1.4;
    const m=st.motif;
    if(m==='stripe'){
      for(let i=-h;i<w;i+=8){ ctx.beginPath(); ctx.moveTo(i,0); ctx.lineTo(i+h,h); ctx.stroke(); }
    } else if(m==='geo'){
      for(let gx=4;gx<w;gx+=12) for(let gy=4;gy<h;gy+=12){
        ctx.beginPath(); ctx.moveTo(gx,gy-4); ctx.lineTo(gx+4,gy); ctx.lineTo(gx,gy+4); ctx.lineTo(gx-4,gy); ctx.closePath(); ctx.stroke();
      }
    } else if(m==='floral'){
      for(let gx=8;gx<w;gx+=14) for(let gy=8;gy<h;gy+=16){
        for(let p=0;p<5;p++){ const a=p*1.256; ctx.beginPath(); ctx.ellipse(gx+Math.cos(a)*3,gy+Math.sin(a)*3,2.4,1.3,a,0,6.28); ctx.fill(); }
      }
    } else if(m==='damask'){
      for(let gy=6;gy<h;gy+=18) for(let gx=8;gx<w;gx+=16){
        ctx.beginPath(); ctx.ellipse(gx,gy,4,7,0,0,6.28); ctx.stroke();
        ctx.beginPath(); ctx.arc(gx,gy-9,2,0,6.28); ctx.stroke();
      }
    } else { // toile — fine cross-hatch scene dashes
      for(let gy=5;gy<h;gy+=7){ for(let gx=3;gx<w;gx+=9){ ctx.beginPath(); ctx.moveTo(gx,gy); ctx.lineTo(gx+3,gy+2); ctx.stroke(); } }
    }
  }

  // Obstacles — dark, ink forms with accent outline
  function drawObstacle(it){
    const x=it.x, y=it.y, w=it.w, h=it.h;
    // shadow
    ctx.globalAlpha=0.35; ctx.fillStyle='#000';
    ctx.beginPath(); ctx.ellipse(x+w/2, ground-3, w*0.6, 5, 0, 0, 6.28); ctx.fill();
    ctx.globalAlpha=1;

    ctx.strokeStyle=T.accent; ctx.lineWidth=1.5;
    if(it.kind==='bucket'){
      // paint bucket (trapezoid) with drip
      ctx.fillStyle='#1a1a20';
      ctx.beginPath();
      ctx.moveTo(x+4,y); ctx.lineTo(x+w-4,y); ctx.lineTo(x+w,y+h); ctx.lineTo(x,y+h); ctx.closePath();
      ctx.fill(); ctx.stroke();
      ctx.fillStyle=T.accent;
      ctx.fillRect(x+4,y,w-8,4);
      // handle
      ctx.beginPath(); ctx.arc(x+w/2,y,w*0.4,Math.PI,0); ctx.stroke();
    } else if(it.kind==='ladder'){
      ctx.fillStyle='#16161b';
      ctx.strokeStyle='#2a2a33'; ctx.lineWidth=1; ctx.fillRect(x,y,w,h); 
      ctx.strokeStyle=T.accentDeep; ctx.lineWidth=3;
      ctx.beginPath(); ctx.moveTo(x+6,y); ctx.lineTo(x+6,y+h); ctx.moveTo(x+w-6,y); ctx.lineTo(x+w-6,y+h); ctx.stroke();
      ctx.lineWidth=2;
      for(let ry=y+10; ry<y+h; ry+=16){ ctx.beginPath(); ctx.moveTo(x+6,ry); ctx.lineTo(x+w-6,ry); ctx.stroke(); }
    } else { // wet-paint sign — sandwich board
      ctx.fillStyle='#1a1a20';
      ctx.beginPath();
      ctx.moveTo(x+w/2,y); ctx.lineTo(x+w,y+h); ctx.lineTo(x,y+h); ctx.closePath();
      ctx.fill();
      ctx.strokeStyle=T.accent; ctx.lineWidth=1.5; ctx.stroke();
      // accent chevrons (caution) — no letters
      ctx.strokeStyle=T.accent; ctx.lineWidth=2.4;
      for(let c=0;c<2;c++){
        const cy=y+h*0.5+c*10;
        ctx.beginPath(); ctx.moveTo(x+w*0.3,cy); ctx.lineTo(x+w*0.5,cy-6); ctx.lineTo(x+w*0.7,cy); ctx.stroke();
      }
    }
  }

  // helpers
  function roundRect(x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); }
  function shade(hex,f){ const n=parseInt(hex.slice(1),16); let r=(n>>16)&255,g=(n>>8)&255,b=n&255; r=Math.min(255,r*f)|0; g=Math.min(255,g*f)|0; b=Math.min(255,b*f)|0; return 'rgb('+r+','+g+','+b+')'; }

  function updateScore(){
    document.getElementById('scoreVal').textContent = G.score;
    document.getElementById('bestVal').textContent = G.best;
  }

  // overlays
  const startOverlay=document.getElementById('startOverlay');
  const overOverlay=document.getElementById('overOverlay');
  const hint=document.getElementById('hint');
  function show(el){ el.classList.remove('hidden'); }
  function hide(el){ el.classList.add('hidden'); }

  // legend swatches
  (function buildLegend(){
    const box=document.getElementById('legendStart');
    STYLES.forEach(s=>{
      const el=document.createElement('div'); el.className='sw';
      el.innerHTML='<span class="dot" style="background:'+s.color+'"></span>'+s.name;
      box.appendChild(el);
    });
  })();

  // input
  function onJumpKey(e){
    if(e.code==='Space'||e.key===' '||e.key==='ArrowUp'){
      e.preventDefault();
      if(G.state==='playing') jump();
      else if(G.state==='start') startGame();
      else if(G.state==='over') startGame();
    }
  }
  window.addEventListener('keydown', onJumpKey);
  canvas.addEventListener('pointerdown', function(e){ e.preventDefault(); if(G.state==='playing') jump(); });
  document.getElementById('startBtn').addEventListener('click', startGame);
  document.getElementById('restartBtn').addEventListener('click', startGame);
  // tapping the game-over/start card background (not button) also starts
  overOverlay.addEventListener('pointerdown', function(e){ if(e.target===overOverlay) startGame(); });

  // boot
  resize();
  updateScore();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>