// house.jsx — house frame: sky, roof, floors (rooms), full-width ground, garden

// ═════════════════════════════════════════════════════════════
// IMPRESSIONIST RENDERER (Renoir make-over)
//
// The whole exterior is painted as an impressionist canvas: thousands of
// small colour "dabs" (rotated ellipses) juxtaposed in layers, with no hard
// outlines. We push the SVG through feDisplacementMap so every edge breaks up
// into a painted edge, and lay a faint canvas-grain on top. A gentle breeze
// (slow, phase-offset sway) animates the vegetation. Daytime palette is
// Renoir's "Chemin montant dans les hautes herbes": tender greens, lemon
// yellows, powder pinks, lavender, cream. Dawn/dusk/night shift the palette.
//
// Determinism: dabs are generated once from a seeded PRNG (mulberry32) so the
// painting is stable across re-renders — it must not reshuffle on every tick
// or it would shimmer. We memoize per (width band, phase).
// ═════════════════════════════════════════════════════════════
function mulberry32(seed) {
  let a = seed >>> 0;
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}


// Small colour palette used by the foot-of-house vegetation (HouseVegetation)
// and the Ground flower band (footDabs). The main backdrop is now a
// pre-rendered image, but these live decorative layers still need grass /
// flower colours per phase. Kept compact: grass, shade, flower, lilac.
const IMP_PALETTES = {
  day:  { grass:['#9CAE5E','#B6C56E','#C8D27E','#8AA04F','#A9B86A','#7E9A55'], shade:['#6E8A55','#5E7A4B','#7E9A65'], flower:['#E8736B','#F2A4A0','#F5D76E','#FFFFFF','#F0E8D0','#C98BB9','#8FA9D6','#EFB6C8'], lilac:['#C8B6D8','#D6C2E0','#E0D0E8','#BFA8D0'] },
  dawn: { grass:['#A6B468','#BCC878','#C8CE84','#94A85E','#B0BE72'], shade:['#769060','#6A845A','#86986A'], flower:['#F2887E','#F6B4A8','#F8DE84','#FFF4E0','#F0E0C8','#D29CC2','#9CB0D8','#F0C2D2'], lilac:['#D2C0E0','#DCC8E6','#E6D6EC','#C8B2D6'] },
  dusk: { grass:['#8FA850','#A6B85E','#B8C46E','#7E9A48','#9EB05A'], shade:['#5E7A42','#4E6A3A','#6A8A4E'], flower:['#EC7A6E','#F0A498','#F2CE74','#F6ECCE','#E4D4B8','#C290B8','#8AA0CE','#E8AEC2'], lilac:['#C2A8D2','#CEB6DA','#D8C4E0','#B49CCC'] },
  night:{ grass:['#3E5240','#34463A','#46583E','#2E4034','#3A4E40'], shade:['#2A3A2E','#243228','#32443A'], flower:['#8E7E96','#A89AB0','#9AA8C8','#C8C8D8','#B0A8C0','#7E92B8','#8898C0','#A89AB8'], lilac:['#7E72A0','#8E84AC','#9A92B8','#766C98'] },
};

// Scene palettes are derived per (scene, phase): each landscape has its own
// base gradient + colour families, and the phase (dawn/day/dusk/night) shifts
// them warmer/cooler/darker. Built once from a seeded PRNG so the painting is
// stable across re-renders (no shimmer). The renderer composes by REGIONS
// (talus, meadow, foreground…) with a path/stream, depth-scaled brush sizes,
// feathered region edges and directed light — so it reads as a composed
// painting, not a uniform texture.
const IMP_W = 1000, IMP_H = 1400;

// Tint helpers — shift a whole palette toward a phase mood.
function _hx(h){h=h.replace('#','');return [parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)];}
function _rgb(a){return '#'+a.map(c=>Math.max(0,Math.min(255,Math.round(c))).toString(16).padStart(2,'0')).join('');}
function shiftColor(hex, mul, add){
  if(typeof hex!=='string'||hex[0]!=='#'||hex.length<7) return hex;
  const c=_hx(hex);
  return _rgb([c[0]*mul[0]+add[0], c[1]*mul[1]+add[1], c[2]*mul[2]+add[2]]);
}
// phase moods: [rMul,gMul,bMul],[rAdd,gAdd,bAdd]
const PHASE_MOOD = {
  day:   { mul:[1,1,1], add:[0,0,0] },
  dawn:  { mul:[1.05,1.0,0.92], add:[14,8,2] },
  dusk:  { mul:[1.04,0.96,0.92], add:[10,2,4] },
  night: { mul:[0.42,0.46,0.62], add:[6,10,28] },
};
function moodColor(hex, phase){ const m=PHASE_MOOD[phase]||PHASE_MOOD.day; return shiftColor(hex, m.mul, m.add); }
function moodArr(arr, phase){ return arr.map(c=>moodColor(c,phase)); }

// ── geometry helpers ────────────────────────────────────────────────────
function _inPoly(x,y,poly){let c=false;for(let i=0,j=poly.length-1;i<poly.length;j=i++){const xi=poly[i][0],yi=poly[i][1],xj=poly[j][0],yj=poly[j][1];if(((yi>y)!=(yj>y))&&(x<(xj-xi)*(y-yi)/(yj-yi)+xi))c=!c;}return c;}
function _bbox(poly){let x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;for(const[x,y]of poly){x0=Math.min(x0,x);y0=Math.min(y0,y);x1=Math.max(x1,x);y1=Math.max(y1,y);}return[x0,y0,x1,y1];}
const _depth = (y)=>0.5+1.0*((y-IMP_H*0.28)/(IMP_H*0.72));

// A painter holds the dab layers and exposes the composition primitives.
function makePainter(seed){
  let a=seed>>>0;
  const rng=()=>{a|=0;a=(a+0x6D2B79F5)|0;let t=Math.imul(a^(a>>>15),1|a);t=(t+Math.imul(t^(t>>>7),61|t))^t;return((t^(t>>>14))>>>0)/4294967296;};
  const r=(lo,hi)=>lo+rng()*(hi-lo);
  const pick=(arr)=>arr[(rng()*arr.length)|0];
  const L={sky:'',far:'',shade:'',mid:'',water:'',path:'',trees:'',fg:'',tall:'',flowers:'',accent:'',light:''};
  const dab=(k,x,y,rx,ry,rot,f,o)=>{L[k]+=`<ellipse cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" rx="${rx.toFixed(1)}" ry="${ry.toFixed(1)}" fill="${f}" opacity="${o.toFixed(2)}" transform="rotate(${rot.toFixed(0)} ${x.toFixed(1)} ${y.toFixed(1)})"/>`;};
  const feather=(x,y,poly,f)=>{if(!_inPoly(x,y,poly))return _inPoly(x+r(-f,f),y+r(-f,f),poly)?0.35:0;return 1;};
  function grass(layer,poly,cols,n,ang,av,lmin,lmax,ft=24){const[x0,y0,x1,y1]=_bbox(poly);let p=0,t=0;while(p<n&&t<n*5){t++;const x=r(x0,x1),y=r(y0,y1);const e=feather(x,y,poly,ft);if(e<=0||rng()>e)continue;const sc=Math.max(0.4,_depth(y));dab(layer,x,y,r(1.2,3)*sc,r(lmin,lmax)*sc,ang+r(-av,av),pick(cols),r(0.5,0.8));p++;}}
  function flowers(layer,poly,cols,n,smin,smax,stem=true,ft=20){const[x0,y0,x1,y1]=_bbox(poly);let p=0,t=0;while(p<n&&t<n*5){t++;const x=r(x0,x1),y=r(y0,y1);const e=feather(x,y,poly,ft);if(e<=0||rng()>e)continue;const sc=Math.max(0.5,_depth(y));if(stem&&rng()<0.4){const sh=r(8,20)*sc;dab(layer,x+r(-1.5,1.5),y+sh*0.4,1.1*sc,sh,r(-12,12),'#7E9A55',r(0.4,0.6));}dab(layer,x,y,r(smin,smax)*sc,r(smin,smax)*sc,r(0,360),pick(cols),r(0.62,0.92));p++;}}
  function tall(poly,cols,n){const[x0,y0,x1,y1]=_bbox(poly);let p=0,t=0;while(p<n&&t<n*5){t++;const x=r(x0,x1),y=r(y0,y1);if(!_inPoly(x,y,poly))continue;const sc=Math.max(0.6,_depth(y));const b=Math.round(r(3,6));for(let i=0;i<b;i++)dab('tall',x+r(-8,8),y,r(1.2,2.4)*sc,r(30,70)*sc,r(-26,22),pick(cols),r(0.5,0.8));p++;}}
  function tree(cx,cy,rad,fol,trunk,dens,lightCols){const n=Math.round(rad*rad/9*dens);for(let i=0;i<n;i++){const ang=r(0,6.28),rr=r(0,rad)*Math.sqrt(rng());dab('trees',cx+Math.cos(ang)*rr*1.05,cy+Math.sin(ang)*rr,r(6,13),r(5,10),r(0,360),pick(fol),r(0.42,0.72));}if(lightCols)for(let i=0;i<n*0.3;i++){const ang=r(Math.PI*1.05,Math.PI*1.6),rr=r(rad*0.3,rad)*Math.sqrt(rng());dab('trees',cx+Math.cos(ang)*rr,cy+Math.sin(ang)*rr,r(4,9),r(3,7),r(0,360),pick(lightCols),r(0.3,0.5));}if(trunk){const dir=rng()<0.5?-1:1,h=rad*1.7;for(let i=0;i<h*0.7;i++){const tt=rng();const bend=Math.sin(tt*Math.PI)*rad*0.12*dir;dab('trees',cx+bend,cy+rad*0.7+tt*h,r(2,5),r(10,18),r(-6,6),pick(trunk),r(0.5,0.8));}}}
  function slim(x,baseY,h,fol,trunk){const dir=rng()<0.5?-1:1;for(let i=0;i<h*0.6;i++){const tt=rng();const bend=Math.sin(tt*Math.PI)*h*0.04*dir;dab('trees',x+bend,baseY-h+tt*h,r(1.6,3),r(7,13),r(-6,6),pick(trunk),r(0.5,0.8));}const cr=h*0.32;for(let i=0;i<cr*cr/6;i++){const ang=r(0,6.28),rr=r(0,cr)*Math.sqrt(rng());dab('trees',x+Math.cos(ang)*rr,baseY-h+Math.sin(ang)*rr,r(4,9),r(4,8),r(0,360),pick(fol),r(0.38,0.64));}}
  function bushF(cx,cy,rad,fol,blossom,bd){for(let i=0;i<rad*rad/8;i++){const ang=r(0,6.28),rr=r(0,rad)*Math.sqrt(rng());dab('trees',cx+Math.cos(ang)*rr,cy+Math.sin(ang)*rr*0.9,r(5,11),r(4,9),r(0,360),pick(fol),r(0.4,0.66));}if(blossom)for(let i=0;i<rad*rad/8*bd;i++){const ang=r(0,6.28),rr=r(0,rad*0.92)*Math.sqrt(rng());dab('trees',cx+Math.cos(ang)*rr,cy+Math.sin(ang)*rr*0.9,r(3,7),r(3,7),r(0,360),pick(blossom),r(0.5,0.8));}}
  function trunkV(x,topY,h,curve,cols){const dir=rng()<0.5?-1:1;for(let i=0;i<h*0.7;i++){const tt=rng();const bend=Math.sin(tt*Math.PI)*curve*dir;dab('trees',x+bend,topY+tt*h,r(2,5),r(10,18),r(-6,6),pick(cols),r(0.5,0.8));}}
  function poplar(x,baseY,h,fol){for(let i=0;i<h*1.3;i++){const tt=rng();const tp=1-tt*0.5;dab('trees',x+r(-h*0.05,h*0.05)*tp,baseY-tt*h,r(3,7)*tp,r(8,15),r(-5,5),pick(fol),r(0.45,0.7));}}
  function poppy(x,y,col){const sc=Math.max(0.6,_depth(y));dab('accent',x+r(-1.5,1.5),y+r(8,18)*sc,1.1*sc,r(10,18)*sc,r(-10,10),'#6E8A4A',r(0.4,0.65));dab('accent',x,y,r(4,6.5)*sc,r(4,6.5)*sc,0,col,r(0.75,0.95));dab('accent',x,y,1.3*sc,1.3*sc,0,'#3A2A2A',0.6);}
  function pathStroke(pts,wTop,wBot,cols){for(let s=0;s<pts.length-1;s++){const[ax,ay]=pts[s],[bx,by]=pts[s+1];for(let i=0;i<34;i++){const tt=i/34;const x=ax+(bx-ax)*tt,y=ay+(by-ay)*tt;const f=(y-pts[0][1])/(pts[pts.length-1][1]-pts[0][1]);const ww=wTop+(wBot-wTop)*f;for(let k=0;k<7;k++)dab('path',x+r(-ww/2,ww/2),y+r(-4,4),r(4,9),r(3,6),r(-20,20),pick(cols),r(0.4,0.7));}}}
  function stream(pts,wTop,wBot,cols){for(let s=0;s<pts.length-1;s++){const[ax,ay]=pts[s],[bx,by]=pts[s+1];for(let i=0;i<40;i++){const tt=i/40;const x=ax+(bx-ax)*tt,y=ay+(by-ay)*tt;const f=(y-pts[0][1])/(pts[pts.length-1][1]-pts[0][1]);const ww=wTop+(wBot-wTop)*f;for(let k=0;k<9;k++)dab('water',x+r(-ww/2,ww/2),y+r(-3,3),r(6,13),r(2,4),r(-8,8),pick(cols),r(0.35,0.65));}}}
  function light(n,y0,y1,cols,op){for(let i=0;i<n;i++)dab('light',r(0,IMP_W),r(y0,y1),r(20,64),r(14,42),r(-20,20),pick(cols),r(op[0],op[1]));}
  function beams(n,cols){for(let i=0;i<n;i++){const x=r(IMP_W*0.2,IMP_W*0.9);dab('light',x,r(IMP_H*0.2,IMP_H*0.7),r(8,20),r(120,260),r(8,20),pick(cols),r(0.04,0.1));}}
  function sky(cols){for(let i=0;i<540;i++)dab('sky',r(0,IMP_W),r(0,IMP_H*0.16+30),r(12,26),r(5,10),r(-8,8),pick(cols),r(0.25,0.5));}
  function fireflies(n,cols){for(let i=0;i<n;i++)dab('light',r(0,IMP_W),r(IMP_H*0.2,IMP_H),r(2,5),r(2,5),0,pick(cols),r(0.4,0.85));}
  return {rng,r,pick,L,dab,inPoly:_inPoly,grass,flowers,tall,tree,slim,bushF,trunkV,poplar,poppy,pathStroke,stream,light,beams,sky,fireflies};
}

// ── scene builders. Each returns nothing; it paints into the painter. ────
// Palettes are written for daytime; moodArr() shifts them per phase.
const SCENE_BUILDERS = {
  meadow(c, ph){
    const W=IMP_W,H=IMP_H,skyH=H*0.13,hz=H*0.29;
    c.sky(moodArr(['#D6E0EA','#E2E8EE','#ECE6E2','#DCE4EA'],ph));
    const talus=[[0,skyH+10],[W*0.52,hz-30],[W*0.46,H*0.56],[0,H*0.5]];
    const coteau=[[W*0.46,hz-30],[W,skyH],[W,H*0.54],[W*0.4,H*0.56]];
    const prairie=[[0,H*0.48],[W*0.45,H*0.54],[W,H*0.5],[W,H*0.76],[0,H*0.74]];
    const fg=[[0,H*0.72],[W,H*0.74],[W,H],[0,H]];
    c.grass('far',talus,moodArr(['#7E9A52','#6E8F4F','#88A05A','#5E7A44','#9CB86A','#506E3E'],ph),2800,18,26,7,15);
    c.grass('far',coteau,moodArr(['#C2CE9A','#D2DAA8','#B6C588','#CCD49E','#DCE0B8','#E2E4C0'],ph),2300,8,28,6,12);
    c.grass('mid',prairie,moodArr(['#AEBE6E','#C2CE82','#CCD68E','#9EB060','#BCC97A','#D4D98C'],ph),4400,-78,26,8,18);
    c.grass('fg',fg,moodArr(['#8AA04F','#9CAE5E','#7E9A4A','#6E8A44','#A9B86A','#5E7A40'],ph),5200,-80,28,12,28);
    c.pathStroke([[W*0.5,H*0.76],[W*0.45,H*0.6],[W*0.5,H*0.46],[W*0.45,hz+6]],48,7,moodArr(['#E8DEC4','#F0E8D2','#DCCEAC','#E4D6B8'],ph));
    c.tree(W*0.15,H*0.23,185,moodArr(['#6E8F4F','#5E7A44','#7FA055','#88A85E','#4E6A3C'],ph),moodArr(['#5A3F28','#6E4E32'],ph),1.1,moodArr(['#B6C97E','#C2CE82'],ph));
    for(const t of [[W*0.6,H*0.46,118],[W*0.7,H*0.44,138],[W*0.8,H*0.47,108],[W*0.88,H*0.43,128],[W*0.66,H*0.5,98],[W*0.94,H*0.46,110]]) c.slim(t[0],t[1],t[2],moodArr(['#88A85E','#A8C06E','#9CB86A','#B6C97E'],ph),moodArr(['#7A5B3A','#8A6A46'],ph));
    c.poplar(W*0.55,H*0.46,260,moodArr(['#6E8F4F','#5E7A48','#7A9A55'],ph));
    c.flowers('flowers',[[0,H*0.72],[W*0.4,H*0.74],[W*0.36,H],[0,H]],moodArr(['#FFFFFF','#F0E8D0','#F5D76E','#FBEAB0','#F2A4A0','#C98BB9'],ph),1200,2.6,5.6);
    c.flowers('flowers',[[W*0.58,H*0.76],[W,H*0.74],[W,H],[W*0.6,H]],moodArr(['#FFFFFF','#F5D76E','#EFB6C8','#C98BB9','#8FA9D6'],ph),1000,2.6,5.6);
    c.tall([[0,H*0.78],[W*0.45,H*0.8],[W*0.4,H],[0,H]],moodArr(['#7E9A4A','#6E8A44','#8AA856','#5E7A40'],ph),90);
    c.tall([[W*0.62,H*0.8],[W,H*0.78],[W,H],[W*0.64,H]],moodArr(['#7E9A4A','#6E8A44','#8AA856','#5E7A40'],ph),70);
    for(let i=0;i<70;i++)c.poppy(c.r(0,W),c.r(H*0.74,H),moodColor('#D8463A',ph));
    c.light(50,H*0.3,H*0.95,ph==='night'?['#C8D4E8','#B8C6E0']:['#FBEAB0','#F5E8B0','#FFF4D0'],ph==='night'?[0.03,0.07]:[0.05,0.12]);
  },
  orchard(c, ph){
    const W=IMP_W,H=IMP_H,skyH=H*0.16,hz=H*0.38;
    c.sky(moodArr(['#DCE8EE','#E8EEF0','#F0E8E8','#E4ECEC'],ph));
    c.grass('far',[[0,skyH],[W,skyH],[W,hz+30],[0,hz+30]],moodArr(['#A6C088','#B8CC92','#94B47C','#C2D29A'],ph),2200,8,26,6,12);
    c.grass('mid',[[0,hz],[W,hz],[W,H*0.7],[0,H*0.7]],moodArr(['#AEC27E','#C2D08A','#B6CC7E','#9EB870','#CCD692'],ph),4600,4,24,8,18);
    c.grass('fg',[[0,H*0.68],[W,H*0.68],[W,H],[0,H]],moodArr(['#9EB870','#B0C27E','#8AA862','#7E9A55','#A8C06E'],ph),5000,-6,26,10,24);
    c.pathStroke([[W*0.5,H],[W*0.5,H*0.7],[W*0.5,hz+20]],90,16,moodArr(['#C8D69A','#D2DCA4','#BECC8E'],ph));
    for(const t of [[W*0.16,H*0.5,120],[W*0.32,H*0.42,96],[W*0.44,H*0.37,76],[W*0.72,H*0.5,120],[W*0.82,H*0.42,96],[W*0.9,H*0.37,76]]){c.tree(t[0],t[1],t[2],moodArr(['#9CB86A','#88A85E','#A8C06E'],ph),moodArr(['#7A5B3A','#8A6A46'],ph),0.7);c.bushF(t[0],t[1],t[2]*0.92,moodArr(['#9CB86A'],ph),moodArr(['#FFFFFF','#F6E0E8','#F0D0DC','#FBEAEE','#FCE8F0'],ph),1.4);}
    c.flowers('flowers',[[0,H*0.7],[W,H*0.7],[W,H],[0,H]],moodArr(['#FFFFFF','#F6E0E8','#F5D76E','#F0D0DC','#FBEAB0','#EFB6C8'],ph),1800,2.6,5.6);
    c.tall([[0,H*0.82],[W,H*0.82],[W,H],[0,H]],moodArr(['#8AA862','#7E9A55','#9EB870','#6E8A4A'],ph),120);
    c.flowers('flowers',[[0,H*0.74],[W,H*0.74],[W,H],[0,H]],moodArr(['#F6E0E8','#FCE8F0','#FFFFFF'],ph),500,2,3.5,false);
    c.light(55,H*0.3,H*0.9,ph==='night'?['#C8D4E8']:['#FBEAB0','#FDF0D8','#FFF4D0'],ph==='night'?[0.03,0.07]:[0.05,0.12]);
  },
  lilac(c, ph){
    const W=IMP_W,H=IMP_H,skyH=H*0.13,hz=H*0.32;
    c.sky(moodArr(['#D6E0DA','#E0E6E2','#E8E4DC','#DCE2D8'],ph));
    c.grass('far',[[0,skyH],[W,skyH],[W,hz+40],[0,hz+40]],moodArr(['#88A86A','#9CB876','#7CA060','#A6C082','#B8C88E'],ph),3000,8,26,6,12);
    c.grass('mid',[[0,hz],[W,hz],[W,H*0.68],[0,H*0.68]],moodArr(['#94B062','#A8BC6E','#B6C87A','#86A456','#9EB866'],ph),5000,2,24,8,18);
    c.grass('fg',[[0,H*0.66],[W,H*0.66],[W,H],[0,H]],moodArr(['#86A456','#9AB066','#7E9A52','#6E8A4A','#A8BC6E'],ph),5000,-6,26,10,24);
    c.stream([[W*0.55,H],[W*0.6,H*0.78],[W*0.5,H*0.6],[W*0.56,hz+20]],70,12,moodArr(['#C2D2D8','#D2DEE2','#B6C8D0','#DCE6E8','#CAD8DC'],ph));
    for(const t of [[W*0.16,H*0.4,150],[W*0.38,H*0.32,118],[W*0.82,H*0.42,158],[W*0.66,H*0.34,108],[W*0.28,H*0.5,90]]) c.bushF(t[0],t[1],t[2],moodArr(['#7FA05A','#6E8F4F','#88A85E','#9CB86A'],ph),moodArr(['#C8B6D8','#D6C2E0','#E0D0E8','#BFA8D0','#FFFFFF','#E8DCF0'],ph),1.6);
    c.tree(W*0.92,H*0.28,140,moodArr(['#7FA05A','#6E8F4F','#88A85E'],ph),moodArr(['#6E4E32','#5A3F28'],ph),0.8,moodArr(['#B6C97E'],ph));
    c.flowers('flowers',[[0,H*0.68],[W,H*0.68],[W,H],[0,H]],moodArr(['#C8B6D8','#FFFFFF','#E0D0E8','#F2A4A0','#D6C2E0','#F5D76E','#EFB6C8'],ph),1900,2.6,5.6);
    c.tall([[0,H*0.8],[W,H*0.8],[W,H],[0,H]],moodArr(['#7E9A52','#6E8A4A','#8AA862','#5E7A40'],ph),110);
    c.light(50,H*0.3,H*0.9,ph==='night'?['#C8D4E8']:['#FBEAB0','#F5E8B0','#FFF4D0'],ph==='night'?[0.03,0.07]:[0.05,0.11]);
  },
  poppy(c, ph){
    const W=IMP_W,H=IMP_H,skyH=H*0.17,hz=H*0.4;
    c.sky(moodArr(['#CFE0EA','#DDE6EC','#E6E6E0','#D6E0E2'],ph));
    c.grass('far',[[0,skyH],[W,skyH],[W,hz+20],[0,hz+20]],moodArr(['#9CB47E','#AEC288','#90AC72','#B6C892'],ph),2200,8,26,6,12);
    c.grass('mid',[[0,hz],[W,hz],[W,H*0.66],[0,H*0.66]],moodArr(['#A6BC6E','#B8C87A','#C2CE82','#9CB466','#AEC074'],ph),4800,2,24,8,18);
    c.grass('fg',[[0,H*0.64],[W,H*0.64],[W,H],[0,H]],moodArr(['#9CB466','#AEC074','#8AA85E','#7E9A52','#B8C87A'],ph),4800,-6,26,10,24);
    c.tree(W*0.15,H*0.3,150,moodArr(['#7FA05A','#88A85E','#6E8F4F'],ph),moodArr(['#6E4E32','#8A6A46'],ph),0.9,moodArr(['#B6C97E'],ph));
    c.tree(W*0.86,H*0.34,120,moodArr(['#88A85E','#9CB86A'],ph),moodArr(['#7A5B3A'],ph),0.8,moodArr(['#C2CE82'],ph));
    for(let i=0;i<900;i++){const y=c.r(hz+30,H);if(c.rng()>((y-hz)/(H-hz)))continue;c.poppy(c.r(0,W),y,moodColor('#E03A2A',ph));}
    for(let i=0;i<350;i++)c.poppy(c.r(0,W),c.r(H*0.6,H),moodColor('#D24A3A',ph));
    c.flowers('flowers',[[0,H*0.6],[W,H*0.6],[W,H],[0,H]],moodArr(['#FFFFFF','#F5D76E','#8FA9D6','#5E7AB8','#E8736B'],ph),1200,2.4,5,true);
    c.tall([[0,H*0.82],[W,H*0.82],[W,H],[0,H]],moodArr(['#8AA85E','#7E9A52','#9CB466','#6E8A4A'],ph),100);
    c.light(55,H*0.3,H*0.95,ph==='night'?['#C8D4E8']:['#FBEAB0','#F5E8B0','#FFF4D0'],ph==='night'?[0.03,0.07]:[0.05,0.12]);
  },
  grove(c, ph){
    const W=IMP_W,H=IMP_H,skyH=H*0.08,hz=H*0.26;
    c.sky(moodArr(['#C8D8D0','#D2DCD2','#BACBB6'],ph));
    c.grass('far',[[0,skyH],[W,skyH],[W,hz+100],[0,hz+100]],moodArr(['#5E7A48','#4E6A40','#6E8F4F','#3E5A38','#7FA05A'],ph),4200,8,30,7,15);
    const clearing=[[W*0.3,H*0.5],[W*0.7,H*0.5],[W*0.85,H],[W*0.15,H]];
    c.grass('mid',[[0,hz],[W,hz],[W,H*0.62],[0,H*0.62]],moodArr(['#6E8A50','#7E9A58','#5E7A48','#88A85E','#6E9050'],ph),5000,2,26,7,16);
    c.grass('mid',clearing,moodArr(['#A2B86E','#B6C87E','#94AC62','#C2CE86','#8AA458'],ph),2600,-4,24,8,18,30);
    for(const t of [[W*0.12,H*0.66],[W*0.26,H*0.7],[W*0.74,H*0.68],[W*0.9,H*0.66],[W*0.42,H*0.5]])c.trunkV(t[0],H*0.1,t[1],20,moodArr(['#6E4E32','#8A6A46','#5A3F28','#7A5B3A'],ph));
    c.tree(W*0.2,H*0.18,200,moodArr(['#5E7A48','#6E8F4F','#7FA055','#4E6A40','#88A85E'],ph),null,1.2,moodArr(['#A8C06E','#B6C97E'],ph));
    c.tree(W*0.78,H*0.16,180,moodArr(['#6E8F4F','#7FA055','#5E7A48','#88A85E'],ph),null,1.2,moodArr(['#A8C06E'],ph));
    c.grass('fg',[[0,H*0.6],[W,H*0.6],[W,H],[0,H]],moodArr(['#6E8A50','#7E9A58','#5E7A48','#4E6A40','#88A05A'],ph),5000,-8,28,10,26);
    c.flowers('flowers',clearing,moodArr(['#FFFFFF','#F5D76E','#C8B6D8','#E8E0C0','#EFB6C8'],ph),1300,2.6,5.4);
    c.tall([[0,H*0.78],[W,H*0.78],[W,H],[0,H]],moodArr(['#5E7A48','#4E6A40','#6E8A50','#7E9A58'],ph),110);
    c.beams(18,ph==='night'?['#B8C6E0','#C8D4E8']:['#F5E8B0','#FBEAB0','#FFF4D0']);
    c.light(60,H*0.4,H*0.9,ph==='night'?['#C8D4E8']:['#F5E8B0','#FBEAB0','#FFF4D0'],ph==='night'?[0.04,0.08]:[0.06,0.14]);
  },
  enchanted(c, ph){
    const W=IMP_W,H=IMP_H,skyH=H*0.14,hz=H*0.32;
    c.sky(moodArr(['#E8D8F0','#D8C8E8','#E0D0E8','#C8B8E0'],ph));
    c.grass('far',[[0,skyH],[W,skyH],[W,hz+50],[0,hz+50]],moodArr(['#8E9CC8','#A2A8D2','#7E8CC0','#B0B0DC','#9CA0CE'],ph),3200,8,26,6,12);
    c.grass('mid',[[0,hz],[W,hz],[W,H*0.66],[0,H*0.66]],moodArr(['#8EA0B8','#9CACC2','#A8B0C8','#86A0A0','#94AEB0'],ph),5000,2,24,8,18);
    c.stream([[W*0.45,H],[W*0.52,H*0.78],[W*0.44,H*0.58],[W*0.5,hz+20]],64,10,moodArr(['#A6E0E0','#C2ECEC','#8ED0D8','#D2F0F0','#B0E4E8'],ph));
    c.tree(W*0.17,H*0.24,185,moodArr(['#8E9CD2','#A6A8DC','#7E8CC8','#C2B6E8','#6E78B8'],ph),moodArr(['#5A4E78','#6A5E88'],ph),1.1,moodArr(['#D8C8EC','#C2B6E8'],ph));
    c.tree(W*0.76,H*0.2,200,moodArr(['#7E9CC0','#92AECE','#A6C2D8','#8EB0C8','#6E92B0'],ph),moodArr(['#4E5A78','#5E6A88'],ph),1.1,moodArr(['#C2E0E8'],ph));
    c.bushF(W*0.48,H*0.42,130,moodArr(['#8EA0C8','#A2B0D2'],ph),moodArr(['#E8D8F0','#F0E0F8','#D8C8EC','#FBF0FF'],ph),1.6);
    c.grass('fg',[[0,H*0.64],[W,H*0.64],[W,H],[0,H]],moodArr(['#7E94B0','#8EA0B8','#6E8AA0','#9CACC2','#7EA0A8'],ph),4800,-6,26,10,24);
    c.flowers('flowers',[[0,H*0.66],[W,H*0.66],[W,H],[0,H]],moodArr(['#E8D8F0','#C2B6E8','#A6C2D8','#FFFFFF','#D8C8EC'],ph),1700,2.6,5.4);
    c.tall([[0,H*0.8],[W,H*0.8],[W,H],[0,H]],moodArr(['#6E8AA0','#7E94B0','#5E7A90','#8EA0B8'],ph),100);
    c.fireflies(160,['#FBEAB0','#FFF4D0','#FFFFFF','#F5E8B0']);
    c.light(40,H*0.2,H,['#E0D0F0','#FBEAB0','#D8E8F0'],[0.04,0.1]);
  },
  bluehour(c, ph){
    const W=IMP_W,H=IMP_H,skyH=H*0.16,hz=H*0.36;
    c.sky(['#46568E','#3A4E86','#5266A0','#34466E','#6A7EA8']);
    c.dab('sky',W*0.74,H*0.13,30,30,0,'#F0EAD0',0.9);for(let i=0;i<40;i++)c.dab('sky',W*0.74,H*0.13,c.r(34,70),c.r(34,70),0,'#E8E4C8',c.r(0.03,0.08));
    c.grass('far',[[0,skyH],[W,skyH],[W,hz+40],[0,hz+40]],['#3E5266','#46586E','#384C60','#506678','#445A6E'],3000,8,26,6,12);
    c.grass('mid',[[0,hz],[W,hz],[W,H*0.66],[0,H*0.66]],['#46606E','#52707E','#3E5A64','#5A7886','#486A72'],5200,2,24,8,18);
    const pond=[[W*0.34,H*0.62],[W*0.66,H*0.62],[W*0.72,H*0.82],[W*0.28,H*0.82]];
    for(let i=0;i<1400;i++){const x=c.r(W*0.28,W*0.72),y=c.r(H*0.62,H*0.82);if(!c.inPoly(x,y,pond))continue;c.dab('water',x,y,c.r(8,18),c.r(2,4),c.r(-6,6),c.pick(['#5A7290','#46607E','#6A82A0','#3E5670']),c.r(0.4,0.7));}
    for(let i=0;i<60;i++)c.dab('water',W*0.5+c.r(-30,30),c.r(H*0.64,H*0.8),c.r(6,16),c.r(1.6,3),c.r(-4,4),'#D8DCC0',c.r(0.2,0.45));
    c.tree(W*0.16,H*0.26,180,['#3A5260','#46606E','#34505C','#52707E','#2E4452'],['#2E3A3C','#3A4648'],1.1,['#6A8290']);
    c.bushF(W*0.84,H*0.42,150,['#3E5A60','#486A70'],['#9CB0D8','#B0C2E0','#8EA4CE','#C2D0E8'],1.5);
    c.grass('fg',[[0,H*0.64],[W,H*0.64],[W,H],[0,H]],['#3E5A64','#486A72','#34505A','#52707E','#2E4850'],4800,-6,26,10,24);
    c.flowers('flowers',[[0,H*0.66],[W,H*0.66],[W,H],[0,H]],['#9CB0D8','#C2D4E8','#B0C2E0','#FFFFFF','#8EA4CE'],1500,2.6,5.2);
    c.tall([[0,H*0.8],[W,H*0.8],[W,H],[0,H]],['#34505A','#3E5A64','#2E4850','#486A72'],100);
    c.fireflies(130,['#C2D4E8','#D8E4F0','#FFFFFF','#A6C0DC']);
  },
};

// Base gradient per scene (daytime), shifted by phase.
const SCENE_BASE = {
  meadow:   ['#DCE4EC','#D2D8CE','#BEC986','#A2B468','#8AA258'],
  orchard:  ['#E8EEF0','#E0E4DC','#CAD6A8','#B4C68E','#A2B67C'],
  lilac:    ['#E0E6E2','#D6DCCE','#BAC890','#A4B67C','#92AA6A'],
  poppy:    ['#DDE6EC','#D6DAD0','#B6C880','#AEC074','#A4B86A'],
  grove:    ['#C8D8D0','#B0C4A6','#86A074','#688A5C','#52704A'],
  enchanted:['#D8C8E8','#BFA8DC','#9A8CC6','#7A72AE','#5E5896'],
  bluehour: ['#3A4E86','#33446C','#3A5070','#445E72','#4E6E78'],
};
const SCENE_SEEDS = { meadow:101, orchard:202, lilac:303, poppy:404, grove:505, enchanted:606, bluehour:707 };
const SCENE_LIST = ['meadow','orchard','lilac','poppy','grove','enchanted','bluehour'];

// Build a scene's painted layers for (scene, phase). Returns { L, base }.
function buildScene(sceneName, phase){
  const name = SCENE_BUILDERS[sceneName] ? sceneName : 'meadow';
  // bluehour is intrinsically night; we don't re-darken it.
  const ph = name === 'bluehour' ? 'day' : (phase || 'day');
  const c = makePainter(SCENE_SEEDS[name] + (name==='bluehour'?0:({day:0,dawn:1,dusk:2,night:3})[ph]||0));
  SCENE_BUILDERS[name](c, ph);
  const base = name === 'bluehour' ? SCENE_BASE.bluehour : moodArr(SCENE_BASE[name], ph);
  return { L: c.L, base };
}

// ImpressionistScene — the full painted backdrop. `scene` selects the
// landscape; `phase` shifts its mood. Composed by regions with breeze on the
// vegetation layers. Painted edges via feDisplacementMap; canvas grain on top.
// The painted backdrop is a pre-rendered image (one per scene/phase) — this is
// far lighter than generating ~30k SVG dabs live in the DOM (which choked the
// browser) and looks identical. Over the image we keep ONE light, living layer:
// tall grass blades at the foot that sway in the breeze, so the scene still
// breathes. The image lives at /img/scenes/scene_{scene}_{phase}.jpg; dawn/dusk
// reuse the day image (a soft CSS tint conveys the time of day elsewhere).
const SCENE_IMG_PHASE = (phase) => (phase === 'night' ? 'night' : 'day');
function ImpressionistScene({ scene, phase }) {
  const sc = (typeof SCENE_BUILDERS !== 'undefined' && SCENE_BUILDERS[scene]) ? scene : 'meadow';
  const imgPhase = sc === 'bluehour' ? 'night' : SCENE_IMG_PHASE(phase);
  const imgUrl = `/img/scenes/scene_${sc}_${imgPhase}.jpg`;
  // Light living foreground: only the tall-grass layer, regenerated live.
  const tall = React.useMemo(() => {
    try { return buildScene(sc, sc === 'bluehour' ? 'day' : (phase || 'day')).L.tall; }
    catch (e) { return ''; }
  }, [sc, phase]);
  // For dawn/dusk, lay a soft warm/cool veil over the day image.
  const veil = phase === 'dawn'
    ? 'linear-gradient(180deg, rgba(252,227,179,0.22), rgba(217,166,200,0.12))'
    : phase === 'dusk'
    ? 'linear-gradient(180deg, rgba(247,185,140,0.22), rgba(110,90,147,0.16))'
    : 'none';
  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 0, overflow: 'hidden' }} aria-hidden="true">
      <img src={imgUrl} alt="" draggable="false" style={{
        position: 'absolute', inset: 0, width: '100%', height: '100%',
        objectFit: 'cover', objectPosition: 'center bottom', display: 'block' }}/>
      {veil !== 'none' && <div style={{ position: 'absolute', inset: 0, background: veil, pointerEvents: 'none' }}/>}
      {/* Living foreground: tall grass swaying in the breeze. No SVG filter
          here on purpose — a filter re-evaluated every animation frame is what
          made the page lag; the CSS transform alone runs on the GPU and the
          painted grain already lives in the background image. */}
      <svg viewBox={`0 0 ${IMP_W} ${IMP_H}`} preserveAspectRatio="xMidYMax slice"
        width="100%" height="100%" style={{ position: 'absolute', inset: 0, display: 'block' }}>
        <g className="imp-breeze imp-breeze-c" dangerouslySetInnerHTML={{ __html: tall }}/>
      </svg>
      <style>{`
        @keyframes impSwayC { 0%,100%{transform:skewX(0deg) translateX(0)} 50%{transform:skewX(0.8deg) translateX(4px)} }
        .imp-breeze { transform-origin: 50% 100%; will-change: transform; transform: translateZ(0); }
        .imp-breeze-c { animation: impSwayC 7s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) { .imp-breeze { animation: none !important; } }
      `}</style>
    </div>
  );
}


// ─────────────────────────────────────────────────────────────
// Sky phase — ambient "the house is alive" layer.
//
// By default the sky follows the *visitor's* real local time, so a house
// looks different in the morning vs the evening (this is what makes it feel
// lived-in rather than a frozen museum). The owner's manual night toggle
// still wins: when they've switched the lights off (night === true) we always
// show night. Phases: dawn → day → dusk → night.
//
// prefers-reduced-motion is respected by the individual animations (clouds,
// birds), not here — the gradient itself is static.
// ─────────────────────────────────────────────────────────────
function currentSkyPhase() {
  const h = new Date().getHours();
  if (h >= 6  && h < 9)  return 'dawn';
  if (h >= 9  && h < 18) return 'day';
  if (h >= 18 && h < 21) return 'dusk';
  return 'night';
}

// Resolve the phase to actually render, given the owner's manual override.
// night === true  → forced night (owner switched the lights off)
// otherwise        → follow the visitor's real local time
function resolveSkyPhase(nightOverride) {
  if (nightOverride) return 'night';
  return currentSkyPhase();
}

// Background gradient painted behind the house for a given phase. 'day'
// returns null so the parent (.house-stage-day, the owner's palette) shows
// through unchanged — we only paint when we're tinting morning/evening/night.
function skyGradientFor(phase) {
  switch (phase) {
    case 'dawn':
      return 'linear-gradient(180deg, #FCE3B3 0%, #F7C8B0 38%, #EBA9B8 72%, #D9A6C8 100%)';
    case 'dusk':
      return 'linear-gradient(180deg, #F7B98C 0%, #E8907E 36%, #B5708F 68%, #6E5A93 100%)';
    case 'night':
      return 'radial-gradient(120% 80% at 75% 10%, #2D3F78 0%, #19264A 45%, #0B132E 85%)';
    case 'day':
    default:
      return null; // let the parent palette show through
  }
}

// ─────────────────────────────────────────────────────────────
// House — orchestrates sky, ground, roof, floors
// ─────────────────────────────────────────────────────────────
function House({
  rooms, layout, houseName, signature, mailboxCount,
  roofStyle, roofColor, roofPattern, chimney, smoke, antenna, doorColor,
  gardenItems,
  editMode, night,
  onTapRoom, onTapMailbox, onAddRoom, onDeleteRoom, onMoveRoom, onAddToSlot,
  onEditGarden, onEditRoof, onTapItem, onOpenNeighbours, onRollDice,
  onKnock, canKnock, visitCount, scene,
}) {
  const SLOTS_PER_FLOOR = 2;
  const ROOM_W = 152;
  const ROOM_H = 118;

  const floors = layout;
  const houseW = SLOTS_PER_FLOOR * ROOM_W;
  const rmById = Object.fromEntries(rooms.map(r => [r.id, r]));

  // The sky follows the visitor's real local time unless the owner forced
  // night. We recompute every few minutes so a long visit drifts naturally
  // (e.g. crossing into the evening) instead of staying frozen.
  const [, tick] = React.useReducer((n) => n + 1, 0);
  React.useEffect(() => {
    const id = setInterval(tick, 5 * 60 * 1000);
    return () => clearInterval(id);
  }, []);
  const phase = resolveSkyPhase(night);
  const isNight = phase === 'night';
  const skyGradient = skyGradientFor(phase);

  return (
    <div style={{ position:'relative', width:'100%', minHeight:'100%' }}>
      {/* Impressionist painted backdrop (Renoir): sky → far hills → meadow →
          trees → flowering grass, all dabs + breeze. Replaces the old flat
          sky/ground gradients. Phase shifts the palette (dawn/day/dusk/night). */}
      <ImpressionistScene scene={scene} phase={phase}/>

      {/* Stacked content: spacer → roof → floors → ground (ground takes full width) */}
      <div style={{ position:'relative', zIndex:1, display:'flex', flexDirection:'column',
        alignItems:'center', minHeight:'100%', boxSizing:'border-box' }}>
        <div style={{ flex:1, minHeight: 96 }}/>

        <Roof w={houseW} style={roofStyle} name={houseName} signature={signature}
          color={roofColor} pattern={roofPattern} chimney={chimney} smoke={smoke} antenna={antenna}
          onTap={onEditRoof} editMode={editMode}/>

        {/* House body — kept readable (a light sketch line, since the rooms
            inside hold the user's content) but softened into the painting:
            an ink-sketch border instead of heavy black, a soft coloured
            shadow instead of the hard offset block, and a faint warm halo so
            the building sits *in* the meadow light rather than on top of it. */}
        <div style={{ position:'relative', width: houseW,
          border:'2px solid #4A3B33', borderTop: 0, background:'#4A3B33',
          borderRadius: '0 0 3px 3px',
          boxShadow:'0 10px 30px rgba(74,59,51,0.28), 0 2px 8px rgba(74,59,51,0.18)' }}>
          {/* warm light wash over the façade, like sun on plaster */}
          <div style={{ position:'absolute', inset:0, zIndex:3, pointerEvents:'none',
            background:'radial-gradient(120% 80% at 30% 0%, rgba(255,243,214,0.18), rgba(255,243,214,0) 60%)',
            mixBlendMode:'soft-light' }}/>
          {floors.map((floor, fi) => (
            <Floor key={fi}
              floorIdx={fi}
              ids={floor}
              roomW={ROOM_W} roomH={ROOM_H}
              rmById={rmById}
              editMode={editMode}
              onTapRoom={onTapRoom}
              onDeleteRoom={onDeleteRoom}
              onMoveRoom={onMoveRoom}
              onAddToSlot={onAddToSlot}
              totalFloors={floors.length}
            />
          ))}
          <FrontDoor doorColor={doorColor} onKnock={onKnock} canKnock={canKnock}/>
          {/* Vegetation that grows IN FRONT of the foot of the house — tall
              grass blades and flowers rising over the bottom edge of the
              rooms. This is what "stitches" the building into the meadow so it
              reads as standing in the painting rather than pasted on top.
              Non-interactive (pointer-events:none) so room taps still work,
              and it only covers the lower band so content stays readable. */}
          <HouseVegetation width={houseW} phase={phase}/>
        </div>

        {/* Edit chips — only when in edit mode */}
        {editMode && (
          <div style={{ position:'absolute', right: 12, top: 100, display:'flex', flexDirection:'column', gap:8, zIndex:9 }}>
            <EditChip onClick={() => onAddRoom('any')}>+ room</EditChip>
            <EditChip onClick={() => onAddRoom('floor')}>+ floor</EditChip>
            <EditChip onClick={onEditRoof}>edit roof</EditChip>
            <EditChip onClick={onEditGarden}>edit garden</EditChip>
          </div>
        )}

        {/* Ground sits at the very bottom, full width */}
        <Ground night={isNight} mailboxCount={mailboxCount} onTapMailbox={onTapMailbox}
          editMode={editMode} gardenItems={gardenItems} onEditGarden={onEditGarden}
          onTapItem={onTapItem} onOpenNeighbours={onOpenNeighbours} onRollDice={onRollDice}/>
      </div>
    </div>
  );
}

// Paint a cluster of impressionist dabs filling a polygon's bounding band,
// clipped to the shape. Used to give the roof a hand-painted shimmer over its
// base colour while keeping a light sketch outline. Deterministic per id.
function roofDabs(seedKey, x0, y0, x1, y1, color) {
  const rng = mulberry32(hashStr(seedKey));
  const r = (a, b) => a + rng() * (b - a);
  // derive a few tints around the base roof colour
  const tints = roofTints(color);
  let s = '';
  const n = Math.round((x1 - x0) * (y1 - y0) / 55);
  for (let i = 0; i < n; i++) {
    const x = r(x0, x1), y = r(y0, y1);
    s += `<ellipse cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" rx="${r(3,7).toFixed(1)}" ry="${r(2,4).toFixed(1)}" fill="${tints[(rng()*tints.length)|0]}" opacity="${r(0.25,0.55).toFixed(2)}" transform="rotate(${(r(-16,16)).toFixed(0)} ${x.toFixed(1)} ${y.toFixed(1)})"/>`;
  }
  return s;
}
function hashStr(s) { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; }
// Build light/dark tints of a hex colour for painterly variation.
function roofTints(hex) {
  const h = hex.replace('#', '');
  if (h.length < 6) return [hex, hex];
  const R = parseInt(h.slice(0,2),16), G = parseInt(h.slice(2,4),16), B = parseInt(h.slice(4,6),16);
  const mix = (f) => '#' + [R,G,B].map(c => Math.max(0,Math.min(255, Math.round(c + f))).toString(16).padStart(2,'0')).join('');
  return [mix(28), mix(14), hex, mix(-18), mix(-34), '#F4E8CE'];
}

// Vegetation that overlaps the foot of the house: tall painted grass blades
// and dense flower clumps rising over the bottom edge of the rooms, plus a
// little climbing growth up the corners. Deterministic, breeze-animated,
// pointer-events:none so it never blocks interaction. Sized to the house
// width; sits at the bottom of the body with a band that overflows downward.
function HouseVegetation({ width, phase }) {
  const VW = 1000, VH = 220;            // virtual canvas; scales to the house width
  const dabs = React.useMemo(() => {
    const P = IMP_PALETTES[phase] || IMP_PALETTES.day;
    const rng = mulberry32(515 + phase.length);
    const r = (a, b) => a + rng() * (b - a);
    const pick = (arr) => arr[(rng() * arr.length) | 0];
    const grassCols = P.grass.concat(P.shade);
    const flowerCols = P.flower.concat(P.lilac);
    let blades = '', flowers = '';
    // tall grass blades along the whole foot, rising up over the bottom edge
    for (let i = 0; i < 520; i++) {
      const x = r(-10, VW + 10), y = r(VH * 0.35, VH);
      blades += `<ellipse cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" rx="${r(2,4).toFixed(1)}" ry="${r(18,46).toFixed(1)}" fill="${pick(grassCols)}" opacity="${r(0.5,0.82).toFixed(2)}" transform="rotate(${r(-22,16).toFixed(0)} ${x.toFixed(1)} ${y.toFixed(1)})"/>`;
    }
    // flower heads scattered among the blades
    for (let i = 0; i < 240; i++) {
      const x = r(0, VW), y = r(VH * 0.4, VH);
      flowers += `<ellipse cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" rx="${r(3,6).toFixed(1)}" ry="${r(3,6).toFixed(1)}" fill="${pick(flowerCols)}" opacity="${r(0.6,0.92).toFixed(2)}" transform="rotate(${r(0,360).toFixed(0)} ${x.toFixed(1)} ${y.toFixed(1)})"/>`;
    }
    // a couple of climbing tufts up the left & right corners
    for (let i = 0; i < 120; i++) {
      const side = rng() < 0.5 ? r(0, 90) : r(VW - 90, VW);
      const y = r(-40, VH);
      blades += `<ellipse cx="${side.toFixed(1)}" cy="${y.toFixed(1)}" rx="${r(2,4).toFixed(1)}" ry="${r(10,26).toFixed(1)}" fill="${pick(grassCols)}" opacity="${r(0.4,0.7).toFixed(2)}" transform="rotate(${r(-18,18).toFixed(0)} ${side.toFixed(1)} ${y.toFixed(1)})"/>`;
    }
    return { blades, flowers };
  }, [phase]);

  return (
    <div aria-hidden="true" style={{
      position: 'absolute', left: 0, right: 0, bottom: -54, height: 150,
      pointerEvents: 'none', zIndex: 4, overflow: 'visible',
    }}>
      <svg viewBox={`0 0 ${VW} ${VH}`} preserveAspectRatio="none"
        width="100%" height="100%" style={{ display: 'block' }}>
        <g className="imp-breeze imp-breeze-c"
          dangerouslySetInnerHTML={{ __html: dabs.blades + dabs.flowers }}/>
      </svg>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Roof — peaked/flat, color, pattern, chimney+smoke, antenna
// ─────────────────────────────────────────────────────────────
function Roof({ w, style, name, signature, color = '#FFFFFF', pattern = 'shingles',
  chimney = true, smoke = true, antenna = true, onTap, editMode }) {
  const overhang = 14;
  const totalW = w + overhang*2;
  const roofH = style === 'flat' ? 56 : 78;

  return (
    <div style={{ position:'relative', width: totalW, marginLeft: -overhang,
      marginBottom: style === 'flat' ? 0 : -3,
      filter: 'drop-shadow(0 6px 14px rgba(74,59,51,0.22))',
      cursor: editMode ? 'pointer' : 'default' }}
      onClick={editMode ? onTap : undefined}>
      {style === 'flat'
        ? <RoofFlat totalW={totalW} overhang={overhang} color={color} pattern={pattern}/>
        : <RoofPeaked totalW={totalW} apex={roofH} color={color} pattern={pattern}/>}

      {/* Antenna — planted ON the roof surface. We compute the roof's
          surface height at the antenna's horizontal position so the base
          sits on the slope instead of floating above it. The pole then
          rises above the roof with the little red ball on top. */}
      {antenna && (() => {
        const antF = 0.66;                 // horizontal position (fraction of width)
        const antX = totalW * antF;
        // Surface y at antX, in the roof SVG's own coordinates:
        const surfaceY = style === 'flat'
          // flat roof top edge runs from (overhang*0.6, 14) to (totalW-overhang*0.6, 2)
          ? 14 + (2 - 14) * Math.min(1, Math.max(0,
              (antX - overhang * 0.6) / ((totalW - overhang * 0.6) - overhang * 0.6)))
          // peaked roof right slope: y = 2 at center → apex at edge
          : 2 + (roofH - 2) * (2 * antF - 1);
        // Pole is 26 tall; place its base 2px into the roof for a planted look.
        return <Antenna left={antX} top={surfaceY - 24}/>;
      })()}

      {/* Chimney (with optional smoke) */}
      {chimney && <Chimney left={overhang + w*0.18} top={style === 'flat' ? 6 : 14} smoke={smoke}/>}

      {/* Nameplate */}
      <div style={{ position:'absolute',
        top: style === 'flat' ? roofH/2 - 10 : roofH * 0.50,
        left:'50%', transform:'translateX(-50%)' }}>
        <NamePlate name={name} signature={signature}/>
      </div>
    </div>
  );
}

function RoofPeaked({ totalW, apex, color, pattern }) {
  const patternId = 'roof_p_' + pattern;
  const clipId = 'roofclip_p';
  return (
    <svg viewBox={`0 0 ${totalW} ${apex+6}`} width={totalW} height={apex+6} style={{ display:'block' }}>
      <defs>
        <RoofPattern id={patternId} kind={pattern} color={color}/>
        <clipPath id={clipId}><polygon points={`0,${apex} ${totalW/2},2 ${totalW},${apex}`}/></clipPath>
        <filter id="roofPaintP" x="-6%" y="-6%" width="112%" height="112%">
          <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="2" seed="5" result="n"/>
          <feDisplacementMap in="SourceGraphic" in2="n" scale="3.5"/>
        </filter>
      </defs>
      <g filter="url(#roofPaintP)">
        <polygon points={`0,${apex} ${totalW/2},2 ${totalW},${apex}`}
          fill={`url(#${patternId})`} stroke="#4A3B33" strokeWidth="2" strokeLinejoin="round"/>
        <g clipPath={`url(#${clipId})`}
          dangerouslySetInnerHTML={{ __html: roofDabs('peak_' + color, 0, 2, totalW, apex, color) }}/>
      </g>
      <rect x="0" y={apex} width={totalW} height="5" fill="#4A3B33"/>
    </svg>
  );
}

function RoofFlat({ totalW, overhang, color, pattern }) {
  const h = 56;
  const patternId = 'roof_f_' + pattern;
  const clipId = 'roofclip_f';
  const pts = `${overhang*0.4},${h} ${overhang*0.6},14 ${totalW-overhang*0.6},2 ${totalW-overhang*0.4},${h}`;
  return (
    <svg viewBox={`0 0 ${totalW} ${h+6}`} width={totalW} height={h+6} style={{ display:'block' }}>
      <defs>
        <RoofPattern id={patternId} kind={pattern} color={color}/>
        <clipPath id={clipId}><polygon points={pts}/></clipPath>
        <filter id="roofPaintF" x="-6%" y="-6%" width="112%" height="112%">
          <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="2" seed="6" result="n"/>
          <feDisplacementMap in="SourceGraphic" in2="n" scale="3.5"/>
        </filter>
      </defs>
      <g filter="url(#roofPaintF)">
        <polygon points={pts}
          fill={`url(#${patternId})`} stroke="#4A3B33" strokeWidth="2" strokeLinejoin="round"/>
        <g clipPath={`url(#${clipId})`}
          dangerouslySetInnerHTML={{ __html: roofDabs('flat_' + color, 0, 2, totalW, h, color) }}/>
      </g>
      <rect x="0" y={h} width={totalW} height="5" fill="#4A3B33"/>
    </svg>
  );
}

function RoofPattern({ id, kind, color }) {
  const w = 14, h = 14;
  // Pattern marks are kept soft and semi-transparent so they read as painted
  // texture over the roof colour rather than hard vector lines. The painterly
  // dab layer + displacement filter on the roof do the rest of the work.
  const ink = 'rgba(74,59,51,0.42)';
  if (kind === 'shingles') {
    return (
      <pattern id={id} patternUnits="userSpaceOnUse" width={w} height={h}>
        <rect width={w} height={h} fill={color}/>
        <path d="M0 7 Q3.5 3 7 7 T 14 7" fill="none" stroke={ink} strokeWidth="1.1"/>
      </pattern>
    );
  }
  if (kind === 'dots') {
    return (
      <pattern id={id} patternUnits="userSpaceOnUse" width="10" height="10">
        <rect width="10" height="10" fill={color}/>
        <circle cx="5" cy="5" r="1.3" fill={ink}/>
      </pattern>
    );
  }
  if (kind === 'stripes') {
    return (
      <pattern id={id} patternUnits="userSpaceOnUse" width="12" height="12" patternTransform="rotate(45)">
        <rect width="12" height="12" fill={color}/>
        <rect width="6" height="12" fill="rgba(74,59,51,0.28)"/>
      </pattern>
    );
  }
  if (kind === 'zigzag') {
    return (
      <pattern id={id} patternUnits="userSpaceOnUse" width="14" height="10">
        <rect width="14" height="10" fill={color}/>
        <path d="M0 8 L 7 2 L 14 8" fill="none" stroke={ink} strokeWidth="1.1"/>
      </pattern>
    );
  }
  if (kind === 'tiles') {
    return (
      <pattern id={id} patternUnits="userSpaceOnUse" width="12" height="8">
        <rect width="12" height="8" fill={color}/>
        <path d="M0 0h12M0 8h12M6 0v8" stroke={ink} strokeWidth="1"/>
      </pattern>
    );
  }
  // plain
  return (
    <pattern id={id} patternUnits="userSpaceOnUse" width="1" height="1">
      <rect width="1" height="1" fill={color}/>
    </pattern>
  );
}

function Chimney({ left, top, smoke }) {
  return (
    <div style={{ position:'absolute', left, top, width: 16, height: 28, zIndex: 2 }}>
      <div style={{ width:'100%', height:'100%', background:'#A6512E', border:'2px solid #5A4A40',
        borderRadius:'3px 3px 0 0', position:'relative' }}>
        <div style={{ position:'absolute', top:-4, left:-3, right:-3, height: 6, background:'#222', border:'2px solid #5A4A40', borderRadius: 2 }}/>
      </div>
      {smoke && <Smoke/>}
    </div>
  );
}

function Smoke() {
  return (
    <div style={{ position:'absolute', left: -8, top: -34, width: 28, height: 30, pointerEvents:'none' }}>
      <div style={{ position:'absolute', left: 4, top: 18, width: 8, height: 8, borderRadius:'50%',
        background:'#fff', opacity:0.75, animation:'smokeRise 3s 0s infinite ease-in', border:'1.5px solid rgba(0,0,0,0.3)' }}/>
      <div style={{ position:'absolute', left: 12, top: 22, width: 10, height: 10, borderRadius:'50%',
        background:'#fff', opacity:0.65, animation:'smokeRise 3s 1.1s infinite ease-in', border:'1.5px solid rgba(0,0,0,0.3)' }}/>
      <div style={{ position:'absolute', left: 6, top: 24, width: 7, height: 7, borderRadius:'50%',
        background:'#fff', opacity:0.55, animation:'smokeRise 3s 2.2s infinite ease-in', border:'1.5px solid rgba(0,0,0,0.3)' }}/>
      <style>{`@keyframes smokeRise{0%{transform:translateY(0) scale(0.6);opacity:0}20%{opacity:.7}100%{transform:translateY(-30px) scale(1.4);opacity:0}}`}</style>
    </div>
  );
}

function NamePlate({ name, signature }) {
  return (
    <div style={{
      display:'inline-flex', flexDirection:'column', alignItems:'center',
      background:'rgba(255,250,236,0.9)', border:'1.5px solid rgba(74,59,51,0.5)', borderRadius: 11,
      padding:'4px 16px 5px', boxShadow:'0 3px 10px rgba(74,59,51,0.2)', transform:'rotate(-1deg)',
      WebkitBackdropFilter:'blur(4px)', backdropFilter:'blur(4px)',
      whiteSpace:'nowrap' }}>
      <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 13, color:'#4A3B33', letterSpacing: 0.5,
        whiteSpace:'nowrap' }}>{name || 'maison'}</div>
      {signature && (
        <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 7, color:'rgba(74,59,51,0.6)' }}>@{signature}</div>
      )}
    </div>
  );
}

function Antenna({ left, top }) {
  return (
    <div style={{ position:'absolute', left, top, width: 2, height: 26, background:'#4A3B33', zIndex: 2 }}>
      <div style={{ position:'absolute', top:-6, left:-3, width:8, height:8, background:'#E15B5B', border:'1.5px solid #4A3B33', borderRadius:'50%' }}/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Floor — one row of rooms
// ─────────────────────────────────────────────────────────────
function Floor({ floorIdx, ids, roomW, roomH, rmById, editMode, onTapRoom, onDeleteRoom, onMoveRoom, onAddToSlot, totalFloors }) {
  const SLOTS = 2;
  const cells = Array.from({ length: SLOTS }, (_, ci) => ids[ci]);
  const neighborRoom = ids.map(id => rmById[id]).find(Boolean);
  const neighborFloor = neighborRoom ? ROOM_LIBRARY[neighborRoom.kind]?.floor : null;
  return (
    <div style={{ display:'flex', position:'relative' }}>
      {cells.map((id, ci) => {
        const room = id ? rmById[id] : null;
        const isLast = ci === SLOTS - 1;
        return (
          <div key={ci} style={{ position:'relative', width: roomW, height: roomH,
            borderRight: !isLast ? '2px solid #5A4A40' : 'none' }}>
            {room ? (
              <RoomInterior room={room} w={roomW} h={roomH}
                onTapRoom={() => onTapRoom(room.id)}/>
            ) : (
              <EmptyCell w={roomW} h={roomH} editMode={editMode}
                neighborFloor={neighborFloor}
                onAdd={() => onAddToSlot(floorIdx, ci)}/>
            )}
            {editMode && room && (
              <EditOverlay
                onTap={() => onTapRoom(room.id)}
                onDelete={() => onDeleteRoom(room.id)}
                onMove={(dir) => onMoveRoom(room.id, dir)}
                canMoveLeft={ci > 0}
                canMoveRight={ci < SLOTS - 1}
                canMoveUp={floorIdx > 0}
                canMoveDown={floorIdx < totalFloors - 1}
              />
            )}
          </div>
        );
      })}
    </div>
  );
}

function EditOverlay({ onTap, onDelete, onMove, canMoveLeft, canMoveRight, canMoveUp, canMoveDown }) {
  const btn = (label, action, dis) => (
    <button onClick={(e) => { e.stopPropagation(); !dis && action(); }} disabled={dis}
      style={{
        appearance:'none', width: 22, height: 22, border:'2px solid #5A4A40', borderRadius:5,
        background: dis ? '#eee' : '#fff', color:'#111',
        fontFamily:'"Fredoka", system-ui', fontWeight: 700, fontSize: 11,
        cursor: dis ? 'not-allowed' : 'pointer', padding:0, opacity: dis ? 0.35 : 1,
        boxShadow:'0 2px 6px rgba(74,59,51,0.22)',
      }}>{label}</button>
  );
  return (
    <div style={{ position:'absolute', inset:0, background:'rgba(255,255,255,0.35)',
      display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center',
      gap:6, cursor:'pointer' }} onClick={onTap}>
      <div style={{ display:'flex', gap:4 }}>{btn('↑', () => onMove('up'), !canMoveUp)}</div>
      <div style={{ display:'flex', gap:4 }}>
        {btn('←', () => onMove('left'), !canMoveLeft)}
        {btn('✎', onTap)}
        {btn('→', () => onMove('right'), !canMoveRight)}
      </div>
      <div style={{ display:'flex', gap:4 }}>{btn('↓', () => onMove('down'), !canMoveDown)}</div>
      <button onClick={(e) => { e.stopPropagation(); onDelete(); }}
        style={{ position:'absolute', top:-8, right:-8, width:22, height:22, borderRadius:'50%',
          border:'2px solid #5A4A40', background:'#E15B5B', color:'#fff', fontWeight:700, fontSize:13,
          cursor:'pointer', padding:0, lineHeight:1, boxShadow:'0 2px 6px rgba(74,59,51,0.22)' }}>×</button>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Front door
// ─────────────────────────────────────────────────────────────
function FrontDoor({ doorColor, onKnock, canKnock }) {
  useHouseLang();
  const doorW = 30, doorH = 50;
  const [knocked, setKnocked] = React.useState(false);
  const [shake, setShake] = React.useState(false);
  const handle = (e) => {
    e.stopPropagation();
    if (!canKnock || knocked) return;
    setShake(true);
    setTimeout(() => setShake(false), 500);
    setKnocked(true);
    onKnock?.();
  };
  const interactive = !!(canKnock && onKnock);
  return (
    <div
      onClick={interactive ? handle : undefined}
      role={interactive ? 'button' : undefined}
      aria-label={interactive ? ht('house.knock.label', 'toquer à la porte') : undefined}
      className={shake ? 'stx-door stx-door-knock' : 'stx-door'}
      style={{ position:'absolute', bottom: 0, left:'50%', transform:'translateX(-50%) translateX(28px)',
      width: doorW, height: doorH, background: doorColor || '#A0532B', border:'2px solid #5A4A40',
      borderBottom:0, borderRadius:'6px 6px 0 0', zIndex: 5,
      cursor: interactive ? 'pointer' : 'default' }}>
      <div style={{ position:'absolute', right: 4, top:'52%', width: 4, height: 4, borderRadius:'50%', background:'#4A3B33' }}/>
      <div style={{ position:'absolute', top: 6, left: 4, right: 4, height: 14, border:'2px solid #5A4A40', background:'#FFE08A' }}/>
      {/* "knocked!" confirmation bubble */}
      {knocked && (
        <div style={{ position:'absolute', bottom:'100%', left:'50%', transform:'translateX(-50%)',
          marginBottom: 4, whiteSpace:'nowrap',
          fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 9, color:'#1c0828',
          background:'#fff', border:'2px solid #5A4A40', borderRadius: 6, padding:'2px 6px',
          boxShadow:'0 2px 6px rgba(74,59,51,0.22)' }}>
          {ht('house.knock.done', 'toc toc !')}
        </div>
      )}
      <style>{`
        .stx-door { transition: transform .1s ease; transform-origin: bottom center; }
        @keyframes stxKnock { 0%,100%{transform:translateX(-50%) translateX(28px) rotate(0)} 25%{transform:translateX(-50%) translateX(28px) rotate(-3deg)} 75%{transform:translateX(-50%) translateX(28px) rotate(3deg)} }
        .stx-door-knock { animation: stxKnock .5s ease-in-out; }
        @media (prefers-reduced-motion: reduce){ .stx-door-knock{ animation:none !important; } }
      `}</style>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Ground — full-width strip at the very bottom, with garden items
// ─────────────────────────────────────────────────────────────
function Ground({ night, mailboxCount, onTapMailbox, editMode, gardenItems, onEditGarden, onTapItem, onOpenNeighbours, onRollDice }) {
  useHouseLang();
  // The ground is no longer a flat green band with a hard top rule — the
  // impressionist meadow painted behind the whole stage already shows through
  // here. We keep this layer transparent and just host the interactive bits
  // (path, garden, signpost, dice). A faint extra band of painted dabs gives
  // a little more flowering grass right at the foot of the house so the
  // building reads as standing *in* the meadow.
  const footDabs = React.useMemo(() => {
    const rng = mulberry32(night ? 99001 : 99002);
    const r = (a,b)=>a+rng()*(b-a);
    const P = IMP_PALETTES[night ? 'night' : 'day'];
    const grassCols = P.grass.concat(P.shade);
    const flowerCols = P.flower.concat(P.lilac);
    let s='';
    // tall grass across the foot
    for (let i=0;i<620;i++){ const x=r(0,1000), y=r(0,90); const len=r(7,18); s+=`<ellipse cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" rx="${r(1.6,3.2).toFixed(1)}" ry="${len.toFixed(1)}" fill="${grassCols[(rng()*grassCols.length)|0]}" opacity="${r(0.4,0.7).toFixed(2)}" transform="rotate(${r(-28,16).toFixed(0)} ${x.toFixed(1)} ${y.toFixed(1)})"/>`; }
    // dense flower clumps along the foot (flowerbeds hugging the house)
    const spots=[[140,46,70],[330,52,64],[510,40,72],[690,52,64],[860,46,70]];
    for (const [cx,cy,rad] of spots){ const n=Math.round(rad*2.0); for(let i=0;i<n;i++){ const a=r(0,Math.PI*2), rr=r(0,rad)*Math.sqrt(rng()); const x=cx+Math.cos(a)*rr, y=cy+Math.sin(a)*rr*0.7; s+=`<ellipse cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" rx="${r(2.4,5).toFixed(1)}" ry="${r(2.4,5).toFixed(1)}" fill="${flowerCols[(rng()*flowerCols.length)|0]}" opacity="${r(0.6,0.92).toFixed(2)}"/>`; } }
    return s;
  }, [night]);
  return (
    <div style={{ alignSelf:'stretch', width:'100%', position:'relative', marginTop:-3, zIndex:6 }}>
      <div style={{ position:'relative', height: 90, background: 'transparent', overflow:'visible' }}>
        {/* extra flowering-grass band at the foot of the house (painted) */}
        <svg viewBox="0 0 1000 90" preserveAspectRatio="none"
          width="100%" height="100%" aria-hidden="true"
          style={{ position:'absolute', inset:0, zIndex:0 }}>
          <g className="imp-breeze imp-breeze-c"
            dangerouslySetInnerHTML={{ __html: footDabs }}/>
        </svg>
        {/* path dots up to door — softened: warm painted stones, no hard black */}
        <div style={{ position:'absolute', left:'50%', top:6, transform:'translateX(-50%) translateX(28px)',
          width: 28, display:'flex', flexDirection:'column', alignItems:'center', gap:3, zIndex: 1 }}>
          {[0,1,2,3,4].map(i => (
            <div key={i} style={{ width: 22, height: 7,
              background: night ? '#5b4e34' : '#E4D2A6',
              border:'1.5px solid rgba(74,59,51,0.5)', borderRadius:'50%',
              boxShadow:'0 1px 2px rgba(74,59,51,0.15)' }}/>
          ))}
        </div>

        {/* Garden items */}
        <Garden items={gardenItems} night={night}
          mailboxCount={mailboxCount} onTapMailbox={onTapMailbox}
          editMode={editMode} onEditGarden={onEditGarden} onTapItem={onTapItem}/>

        {/* Neighbourhood and dice were moved OUT of the painted scene into
            the floating glass dock (house-app's FloatingDock) so the chrome
            no longer clashes with the impressionist look. onOpenNeighbours /
            onRollDice are still accepted for compatibility but not rendered
            here anymore. */}
      </div>
      {/* slim foundation line */}
      <div style={{ height: 4, background:'#4A3B33' }}/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// DiceButton — "roll the dice" discovery. A cute die sitting on the grass
// to the left of the path. Tapping it tumbles for a beat, then calls onRoll
// (which fetches a random house and navigates there). The pips shuffle while
// it rolls so it reads as a real throw. Respects reduced-motion.
// ─────────────────────────────────────────────────────────────
function DiceButton({ onRoll }) {
  useHouseLang();
  const [rolling, setRolling] = React.useState(false);
  const [face, setFace] = React.useState(5);
  const timers = React.useRef([]);
  React.useEffect(() => () => { timers.current.forEach(clearTimeout); }, []);

  const handle = () => {
    if (rolling) return;
    setRolling(true);
    // Shuffle the visible face a few times for a "tumbling" feel.
    let n = 0;
    const shuffle = setInterval(() => { setFace(1 + Math.floor(Math.random() * 6)); n++; if (n > 5) clearInterval(shuffle); }, 110);
    const t = setTimeout(() => {
      clearInterval(shuffle);
      setRolling(false);
      onRoll();
    }, 720);
    timers.current.push(t);
  };

  // Pip layouts for faces 1..6 on a 3x3 grid (positions 0..8).
  const PIPS = {
    1: [4], 2: [0, 8], 3: [0, 4, 8], 4: [0, 2, 6, 8], 5: [0, 2, 4, 6, 8], 6: [0, 2, 3, 5, 6, 8],
  };
  const pips = PIPS[face] || PIPS[5];

  return (
    <button
      onClick={handle}
      aria-label={ht('house.dice.label', 'tomber sur une maison au hasard')}
      style={{
        position: 'absolute', left: 'calc(50% - 96px)', bottom: 8,
        appearance: 'none', padding: 0, background: 'transparent', border: 0,
        cursor: rolling ? 'default' : 'pointer', zIndex: 3,
        display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
      }}>
      <div className={rolling ? 'stx-dice stx-dice-rolling' : 'stx-dice'} style={{
        width: 34, height: 34, background: '#fff',
        border: '2px solid #5A4A40', borderRadius: 8, boxShadow: '0 3px 8px rgba(74,59,51,0.22)',
        display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridTemplateRows: 'repeat(3, 1fr)',
        padding: 4, boxSizing: 'border-box',
      }}>
        {Array.from({ length: 9 }).map((_, i) => (
          <span key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            {pips.includes(i) && (
              <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#1c0828' }}/>
            )}
          </span>
        ))}
      </div>
      <span style={{
        fontFamily: '"Fredoka", system-ui', fontWeight: 700, fontSize: 9, color: '#1c0828',
        background: '#fff', border: '2px solid #5A4A40', borderRadius: 5, padding: '1px 6px',
        boxShadow: '0 2px 6px rgba(74,59,51,0.22)', letterSpacing: 0.2, whiteSpace: 'nowrap',
      }}>
        {rolling ? ht('house.dice.rolling', '…') : ht('house.dice.cta', 'au hasard')}
      </span>
      <style>{`
        .stx-dice { transition: transform .12s ease; }
        @keyframes stxRoll {
          0%   { transform: rotate(0)   translateY(0); }
          25%  { transform: rotate(160deg) translateY(-8px); }
          50%  { transform: rotate(300deg) translateY(0); }
          75%  { transform: rotate(420deg) translateY(-4px); }
          100% { transform: rotate(540deg) translateY(0); }
        }
        .stx-dice-rolling { animation: stxRoll .72s cubic-bezier(.3,.7,.4,1) both; }
        @media (prefers-reduced-motion: reduce) {
          .stx-dice-rolling { animation: none !important; }
        }
      `}</style>
    </button>
  );
}

// ─────────────────────────────────────────────────────────────
// Garden — user-customizable items on the grass
// ─────────────────────────────────────────────────────────────
function Garden({ items, night, mailboxCount, onTapMailbox, editMode, onEditGarden, onTapItem }) {
  // Split into left-of-door and right-of-door groups based on item.side.
  // 'auto' items go to whichever side currently has fewer.
  const left = [], right = [];
  (items || []).forEach((it) => {
    if (it.side === 'left') left.push(it);
    else if (it.side === 'right') right.push(it);
    else (left.length <= right.length ? left : right).push(it);
  });

  const renderGroup = (group, side) => (
    <div style={{
      position:'absolute', bottom: 2, [side]: 0,
      width: 'calc(50% - 26px)', // leave room for centered door path
      display:'flex', alignItems:'flex-end', justifyContent: side === 'left' ? 'flex-start' : 'flex-end',
      gap: 4, padding:'0 8px', boxSizing:'border-box', zIndex: 2,
    }}>
      {group.map((it) => (
        <GardenItem key={it.id} item={it} night={night}
          mailboxCount={mailboxCount} onTapMailbox={onTapMailbox} onTapItem={onTapItem}/>
      ))}
    </div>
  );

  return (
    <>
      {renderGroup(left, 'left')}
      {renderGroup(right, 'right')}
      {editMode && (
        <button onClick={onEditGarden} style={{
          position:'absolute', right: 8, top: 8, zIndex: 5,
          appearance:'none', padding:'4px 8px', background:'#FFF8E0',
          border:'2px solid #5A4A40', borderRadius: 999,
          fontFamily:'"Fredoka", system-ui', fontWeight: 600, fontSize: 11, cursor:'pointer',
          boxShadow:'0 2px 6px rgba(74,59,51,0.22)',
        }}>+ jardin</button>
      )}
    </>
  );
}

// ─────────────────────────────────────────────────────────────
// GardenItem — render one item by kind
// ─────────────────────────────────────────────────────────────
function GardenItem({ item, night, mailboxCount, onTapMailbox, onTapItem }) {
  const k = item.kind;
  if (k === 'mailbox') return <Mailbox count={mailboxCount} onTap={onTapMailbox} color={item.color}/>;
  if (k === 'photo')      return <GardenPhoto item={item} onTap={onTapItem}/>;
  if (k === 'link')       return <GardenLink item={item} onTap={onTapItem}/>;
  if (k === 'tree')       return <Tree night={night} color={item.color}/>;
  if (k === 'bush')       return <Bush color={item.color}/>;
  if (k === 'flower')     return <Flower color={item.color || '#E15B5B'}/>;
  if (k === 'mushroom')   return <Mushroom/>;
  if (k === 'pumpkin')    return <Pumpkin/>;
  if (k === 'birdhouse')  return <Birdhouse/>;
  if (k === 'pinwheel')   return <Pinwheel/>;
  if (k === 'gnome')      return <Gnome/>;
  if (k === 'pond')       return <Pond night={night}/>;
  if (k === 'rocks')      return <Rocks/>;
  if (k === 'sun')        return <SunChair/>;
  if (k === 'fence')      return <Fence/>;
  if (k === 'sign')       return <Sign text={item.text || 'bienvenue'}/>;
  return null;
}

// A framed photo on a little stake. Tapping it (in the page, not the editor)
// opens a popover with the full image — see house-app.jsx. Shows a mountain
// placeholder until the owner sets an image URL.
function GardenPhoto({ item, onTap }) {
  return (
    <div onClick={() => onTap && onTap(item)}
      style={{ display:'flex', flexDirection:'column', alignItems:'center', cursor:'pointer' }}>
      <div style={{ width: 42, height: 36, background:'#fff', border:'2px solid #5A4A40',
        borderRadius: 4, boxShadow:'0 3px 8px rgba(74,59,51,0.22)', overflow:'hidden',
        display:'flex', alignItems:'center', justifyContent:'center' }}>
        {item.src
          ? <img src={item.src} alt="" style={{ width:'100%', height:'100%', objectFit:'cover' }}/>
          : <svg viewBox="0 0 42 36" width="42" height="36">
              <rect width="42" height="36" fill="#EADFCE"/>
              <circle cx="31" cy="11" r="4" fill="#FFD159"/>
              <path d="M0 30 L13 17 L23 27 L31 20 L42 30 Z" fill="#9CBF86"/>
            </svg>}
      </div>
      <div style={{ width: 3, height: 16, background:'#4A3B33' }}/>
    </div>
  );
}

// A little signpost. Tapping opens the owner's link in a new tab.
function GardenLink({ item, onTap }) {
  return (
    <div onClick={() => onTap && onTap(item)}
      style={{ display:'flex', flexDirection:'column', alignItems:'center', cursor:'pointer' }}>
      <div style={{ display:'flex', alignItems:'center', gap: 4,
        background:'#FFE08A', border:'2px solid #5A4A40', borderRadius: 5,
        boxShadow:'0 3px 8px rgba(74,59,51,0.22)', padding:'3px 7px', maxWidth: 80 }}>
        <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="#1c0828"
             strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
          <path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/>
          <path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/>
        </svg>
        <span style={{ fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 9,
          color:'#1c0828', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis', maxWidth: 54 }}>
          {item.label || 'lien'}
        </span>
      </div>
      <div style={{ width: 3, height: 16, background:'#4A3B33' }}/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Garden item primitives
// ─────────────────────────────────────────────────────────────
function Mailbox({ count = 0, onTap, color = '#E89D5A' }) {
  return (
    <div onClick={onTap} style={{ display:'flex', flexDirection:'column', alignItems:'center',
      cursor: onTap ? 'pointer' : 'default', position:'relative' }}>
      {count > 0 && (
        <div style={{ position:'absolute', right:-8, top:-4, zIndex:2,
          background:'#E15B5B', color:'#fff', fontFamily:'"Fredoka", system-ui',
          fontWeight:700, fontSize:11, padding:'1px 6px', borderRadius: 999,
          border:'2px solid #5A4A40', boxShadow:'0 2px 6px rgba(74,59,51,0.22)' }}>{count}</div>
      )}
      <div style={{ position:'relative', width: 50, height: 34, background: color,
        border:'2px solid #5A4A40', borderRadius:'20px 20px 4px 4px', boxShadow:'0 3px 8px rgba(74,59,51,0.22)' }}>
        <div style={{ position:'absolute', top:'42%', left:'18%', width:'46%', height: 3.5, background:'#4A3B33', borderRadius:2 }}/>
        <div style={{ position:'absolute', top: 4, right: -4, width: 3, height: 14, background:'#4A3B33' }}/>
        <div style={{ position:'absolute', top: 4, right: -14, width: 10, height: 7, background:'#E15B5B', border:'1.5px solid #4A3B33' }}/>
      </div>
      <div style={{ width: 6, height: 18, background:'#fff', border:'2px solid #5A4A40', borderTop:0 }}/>
    </div>
  );
}

function Bush({ color }) {
  const c = color || '#5E9E51';
  return (
    <svg width="38" height="24" viewBox="0 0 44 28">
      <ellipse cx="10" cy="18" rx="9" ry="9" fill={c} stroke="#4A3B33" strokeWidth="2"/>
      <ellipse cx="22" cy="14" rx="11" ry="11" fill={c} stroke="#4A3B33" strokeWidth="2"/>
      <ellipse cx="34" cy="18" rx="9" ry="9" fill={c} stroke="#4A3B33" strokeWidth="2"/>
      <circle cx="18" cy="12" r="1.5" fill="#E15B5B" stroke="#4A3B33" strokeWidth="1"/>
      <circle cx="26" cy="16" r="1.5" fill="#E15B5B" stroke="#4A3B33" strokeWidth="1"/>
    </svg>
  );
}

function Tree({ night, color }) {
  const leaves = color || (night ? '#345d44' : '#5E9E51');
  return (
    <svg width="50" height="76" viewBox="0 0 60 86">
      <rect x="26" y="48" width="8" height="38" fill="#5C3A1E" stroke="#4A3B33" strokeWidth="2.5"/>
      <ellipse cx="30" cy="32" rx="28" ry="26" fill={leaves} stroke="#4A3B33" strokeWidth="2.5"/>
      <circle cx="18" cy="22" r="2" fill="#E15B5B" stroke="#4A3B33" strokeWidth="1"/>
      <circle cx="42" cy="28" r="2" fill="#E15B5B" stroke="#4A3B33" strokeWidth="1"/>
      <circle cx="30" cy="14" r="2" fill="#E15B5B" stroke="#4A3B33" strokeWidth="1"/>
    </svg>
  );
}

function Flower({ color }) {
  return (
    <div style={{ display:'flex', flexDirection:'column', alignItems:'center', height: 22 }}>
      <svg width="14" height="14" viewBox="0 0 14 14">
        <circle cx="7" cy="3.5" r="2.6" fill={color} stroke="#4A3B33" strokeWidth="1.3"/>
        <circle cx="11" cy="7" r="2.6" fill={color} stroke="#4A3B33" strokeWidth="1.3"/>
        <circle cx="3" cy="7" r="2.6" fill={color} stroke="#4A3B33" strokeWidth="1.3"/>
        <circle cx="7" cy="10" r="2.6" fill={color} stroke="#4A3B33" strokeWidth="1.3"/>
        <circle cx="7" cy="7" r="1.7" fill="#FFE08A" stroke="#4A3B33" strokeWidth="1"/>
      </svg>
      <div style={{ width: 2, height: 8, background:'#3a5a2c' }}/>
    </div>
  );
}

function Mushroom() {
  return (
    <svg width="20" height="22" viewBox="0 0 20 22">
      <path d="M2 10a8 8 0 0116 0H2z" fill="#E15B5B" stroke="#4A3B33" strokeWidth="2"/>
      <circle cx="6" cy="8" r="1.4" fill="#fff"/>
      <circle cx="13" cy="6" r="1.6" fill="#fff"/>
      <circle cx="14" cy="9" r="1.1" fill="#fff"/>
      <rect x="7.5" y="10" width="5" height="9" rx="1" fill="#FFF8E0" stroke="#4A3B33" strokeWidth="2"/>
    </svg>
  );
}

function Pumpkin() {
  return (
    <svg width="26" height="20" viewBox="0 0 26 20">
      <ellipse cx="13" cy="13" rx="11" ry="7" fill="#E89D5A" stroke="#4A3B33" strokeWidth="2"/>
      <path d="M7 13c0-4 0-7 1.5-7M13 13c0-4 0-8 0-8M19 13c0-4 0-7-1.5-7" stroke="#4A3B33" strokeWidth="1.4" fill="none"/>
      <path d="M12 6c0-2 1-3 2-3" stroke="#5C3A1E" strokeWidth="2" fill="none" strokeLinecap="round"/>
    </svg>
  );
}

function Birdhouse() {
  return (
    <svg width="22" height="50" viewBox="0 0 22 50">
      <rect x="9" y="22" width="4" height="28" fill="#5C3A1E" stroke="#4A3B33" strokeWidth="2"/>
      <rect x="3" y="14" width="16" height="12" fill="#FFE08A" stroke="#4A3B33" strokeWidth="2"/>
      <polygon points="3,14 11,4 19,14" fill="#E15B5B" stroke="#4A3B33" strokeWidth="2" strokeLinejoin="round"/>
      <circle cx="11" cy="20" r="2.5" fill="#4A3B33"/>
      <rect x="10" y="22" width="2" height="3" fill="#4A3B33"/>
    </svg>
  );
}

function Pinwheel() {
  return (
    <svg width="22" height="48" viewBox="0 0 22 48">
      <rect x="10" y="18" width="2" height="30" fill="#5C3A1E"/>
      <g style={{ transformOrigin:'11px 14px', animation:'spin 4s linear infinite' }}>
        <path d="M11 14L4 4 L 11 8z" fill="#7BA8D8" stroke="#4A3B33" strokeWidth="1.5"/>
        <path d="M11 14L18 4 L 14 11z" fill="#E5A1A1" stroke="#4A3B33" strokeWidth="1.5"/>
        <path d="M11 14L18 24 L 11 20z" fill="#A3D0A8" stroke="#4A3B33" strokeWidth="1.5"/>
        <path d="M11 14L4 24 L 8 17z" fill="#FFE08A" stroke="#4A3B33" strokeWidth="1.5"/>
        <circle cx="11" cy="14" r="2" fill="#4A3B33"/>
      </g>
      <style>{`@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`}</style>
    </svg>
  );
}

function Gnome() {
  return (
    <svg width="22" height="32" viewBox="0 0 22 32">
      <polygon points="11,2 3,18 19,18" fill="#E15B5B" stroke="#4A3B33" strokeWidth="2" strokeLinejoin="round"/>
      <ellipse cx="11" cy="22" rx="8" ry="6" fill="#FFE2C9" stroke="#4A3B33" strokeWidth="2"/>
      <path d="M5 24 Q11 30 17 24" fill="#fff" stroke="#4A3B33" strokeWidth="2"/>
      <circle cx="9" cy="22" r="1" fill="#4A3B33"/>
      <circle cx="13" cy="22" r="1" fill="#4A3B33"/>
    </svg>
  );
}

function Pond({ night }) {
  return (
    <svg width="46" height="22" viewBox="0 0 46 22">
      <ellipse cx="23" cy="13" rx="22" ry="8" fill={night ? '#1d3a4a' : '#5BB0E1'} stroke="#4A3B33" strokeWidth="2"/>
      <path d="M10 11 q 4 -2 8 0" stroke="#fff" strokeWidth="1.5" fill="none" opacity="0.7"/>
      <path d="M28 14 q 4 -2 8 0" stroke="#fff" strokeWidth="1.5" fill="none" opacity="0.7"/>
      <ellipse cx="34" cy="11" rx="3.5" ry="2" fill="#5E9E51" stroke="#4A3B33" strokeWidth="1.5"/>
      <circle cx="34" cy="11" r="1" fill="#FFE08A"/>
    </svg>
  );
}

function Rocks() {
  return (
    <svg width="28" height="16" viewBox="0 0 28 16">
      <ellipse cx="8" cy="12" rx="7" ry="5" fill="#9B9089" stroke="#4A3B33" strokeWidth="2"/>
      <ellipse cx="20" cy="13" rx="6" ry="4" fill="#7C736C" stroke="#4A3B33" strokeWidth="2"/>
      <path d="M3 11 Q 8 9 13 11" stroke="#fff" strokeWidth="0.8" fill="none" opacity="0.4"/>
    </svg>
  );
}

function SunChair() {
  return (
    <svg width="34" height="20" viewBox="0 0 34 20">
      <path d="M2 18 L 8 8 L 28 8 L 32 18 Z" fill="#FFE08A" stroke="#4A3B33" strokeWidth="2" strokeLinejoin="round"/>
      <path d="M8 8 L 8 18 M 28 8 L 28 18" stroke="#4A3B33" strokeWidth="2"/>
      <rect x="11" y="8" width="14" height="3" fill="#E15B5B" stroke="#4A3B33" strokeWidth="1.2"/>
    </svg>
  );
}

function Fence() {
  return (
    <svg width="34" height="22" viewBox="0 0 34 22">
      {[2, 12, 22].map((x, i) => (
        <polygon key={i} points={`${x},22 ${x},6 ${x+5},2 ${x+10},6 ${x+10},22`}
          fill="#fff" stroke="#4A3B33" strokeWidth="1.6"/>
      ))}
      <rect x="0" y="10" width="34" height="2.5" fill="#fff" stroke="#4A3B33" strokeWidth="1.4"/>
      <rect x="0" y="16" width="34" height="2.5" fill="#fff" stroke="#4A3B33" strokeWidth="1.4"/>
    </svg>
  );
}

function Sign({ text }) {
  return (
    <div style={{ display:'flex', flexDirection:'column', alignItems:'center' }}>
      <div style={{ background:'#FFF8E0', border:'2px solid #5A4A40', borderRadius: 4,
        padding:'2px 8px', boxShadow:'0 2px 6px rgba(74,59,51,0.22)', fontFamily:'"Fredoka", system-ui',
        fontWeight: 700, fontSize: 10, color:'#111', whiteSpace:'nowrap' }}>{text}</div>
      <div style={{ width: 3, height: 14, background:'#5C3A1E', border:'1px solid #4A3B33' }}/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Sky
// ─────────────────────────────────────────────────────────────
function NightSky() {
  const stars = React.useMemo(() => Array.from({ length: 32 }).map((_,i) => ({
    left: `${(i*37 + 7) % 100}%`,
    top: `${(i*23 + 5) % 70}%`,
    size: ((i % 3) + 1),
    delay: (i % 5) * 0.4,
  })), []);
  return (
    <div style={{ position:'absolute', inset:0, zIndex:0, overflow:'hidden', pointerEvents:'none' }}>
      <div style={{ position:'absolute', right: 36, top: 90, width: 54, height: 54, borderRadius:'50%',
        background:'#F5E8B8', border:'2px solid #5A4A40', boxShadow:'0 5px 14px rgba(74,59,51,0.26)' }}>
        <div style={{ position:'absolute', right:8, top:12, width: 36, height: 36, borderRadius:'50%',
          background:'radial-gradient(circle at 30% 30%, transparent 40%, #E1D38A 41%, transparent 55%)' }}/>
        <div style={{ position:'absolute', left:12, top:18, width:6, height:6, borderRadius:'50%', background:'#D6C684'}}/>
        <div style={{ position:'absolute', left:24, top:30, width:4, height:4, borderRadius:'50%', background:'#D6C684'}}/>
      </div>
      {stars.map((s,i)=>(
        <div key={i} style={{ position:'absolute', left:s.left, top:s.top,
          width: s.size*2.5, height: s.size*2.5, background:'#fff', borderRadius:'50%',
          opacity: 0.85, boxShadow:'0 0 4px rgba(255,255,255,0.7)',
          animation: `twinkle 3s ${s.delay}s infinite ease-in-out` }}/>
      ))}
      <style>{`@keyframes twinkle { 0%,100%{opacity:.85;transform:scale(1)} 50%{opacity:.3;transform:scale(.7)} }`}</style>
    </div>
  );
}

function DaySky({ phase = 'day' }) {
  // Sun + drifting clouds over whatever gradient the sky layer / parent
  // painted. The sun shifts colour and height with the phase (low golden sun
  // at dawn/dusk, bright high sun midday). Clouds drift slowly across and
  // loop; a bird crosses now and then. All motion is gentle and offset so it
  // reads as cosy, never busy — and it all pauses for reduced-motion users.
  const dawnDusk = phase === 'dawn' || phase === 'dusk';
  const sunColor = phase === 'dawn' ? '#FFE0A3' : phase === 'dusk' ? '#FF9E5E' : '#FFD159';
  const sunTop   = dawnDusk ? 120 : 90;
  return (
    <div style={{ position:'absolute', inset:0, zIndex:0, overflow:'hidden', pointerEvents:'none' }}>
      <div style={{ position:'absolute', right: 40, top: sunTop, width: 52, height: 52, borderRadius:'50%',
        background: sunColor, border:'2px solid #5A4A40', boxShadow:'0 5px 14px rgba(74,59,51,0.26)',
        transition:'top 1.6s ease, background 1.6s ease' }}/>
      {/* Drifting clouds — each loops across the sky at its own slow pace and
          offset so they never march in lockstep. */}
      <DriftCloud top={120} scale={1}    duration={64} delay={0}/>
      <DriftCloud top={170} scale={0.8}  duration={86} delay={-30}/>
      <DriftCloud top={70}  scale={0.62} duration={104} delay={-62}/>
      {/* A little bird crossing occasionally — only by day, not dawn/dusk. */}
      {phase === 'day' && <Bird/>}
      <style>{`
        @keyframes cloudDrift { from { transform: translateX(-90px); } to { transform: translateX(calc(100vw + 90px)); } }
        @keyframes birdFly { 0%{transform:translate(-40px,0)} 50%{transform:translate(48vw,-14px)} 100%{transform:translate(100vw,4px)} }
        @keyframes birdFlap { 0%,100%{transform:scaleY(1)} 50%{transform:scaleY(0.5)} }
        @media (prefers-reduced-motion: reduce) {
          .stx-drift, .stx-bird { animation: none !important; }
          .stx-drift { transform: none !important; }
          .stx-bird { display: none !important; }
        }
      `}</style>
    </div>
  );
}

// One cloud that drifts left→right forever. We wrap the SVG so the animated
// transform (drift) and the static scale don't fight over `transform`.
function DriftCloud({ top, scale, duration, delay }) {
  return (
    <div className="stx-drift" style={{ position:'absolute', left: 0, top,
      animation:`cloudDrift ${duration}s ${delay}s linear infinite`, willChange:'transform' }}>
      <div style={{ transform:`scale(${scale})`, transformOrigin:'top left' }}>
        <svg width="70" height="30" viewBox="0 0 70 30">
          <ellipse cx="20" cy="20" rx="14" ry="9"  fill="#fff" stroke="#4A3B33" strokeWidth="2.5"/>
          <ellipse cx="38" cy="14" rx="14" ry="11" fill="#fff" stroke="#4A3B33" strokeWidth="2.5"/>
          <ellipse cx="54" cy="20" rx="12" ry="8"  fill="#fff" stroke="#4A3B33" strokeWidth="2.5"/>
        </svg>
      </div>
    </div>
  );
}

function Bird() {
  return (
    <div className="stx-bird" style={{ position:'absolute', top: 60, left: 0,
      animation:'birdFly 22s 6s linear infinite', willChange:'transform' }}>
      <div style={{ animation:'birdFlap 0.5s infinite ease-in-out' }}>
        <svg width="22" height="12" viewBox="0 0 22 12" fill="none" stroke="#1c0828" strokeWidth="2.2" strokeLinecap="round">
          <path d="M2 8 Q6 2 11 7 Q16 2 20 8"/>
        </svg>
      </div>
    </div>
  );
}

// Kept for backward-compat: the old static Cloud is still exported below in
// case anything references it; new sky uses DriftCloud.
function Cloud({ left, top, scale }) {
  return (
    <div style={{ position:'absolute', left, top, transform:`scale(${scale})`, transformOrigin:'top left' }}>
      <svg width="70" height="30" viewBox="0 0 70 30">
        <ellipse cx="20" cy="20" rx="14" ry="9" fill="#fff" stroke="#4A3B33" strokeWidth="2.5"/>
        <ellipse cx="38" cy="14" rx="14" ry="11" fill="#fff" stroke="#4A3B33" strokeWidth="2.5"/>
        <ellipse cx="54" cy="20" rx="12" ry="8" fill="#fff" stroke="#4A3B33" strokeWidth="2.5"/>
      </svg>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Garden catalog (used by AddGardenSheet)
// ─────────────────────────────────────────────────────────────
const GARDEN_LIBRARY = {
  mailbox:    { label: 'Boîte aux lettres',    hasColor: true,  defaultColor: '#E89D5A' },
  photo:      { label: 'Photo',       hasColor: false, hasPhoto: true },
  link:       { label: 'Lien',        hasColor: false, hasLink: true },
  tree:       { label: 'Arbre',       hasColor: true,  defaultColor: '#5E9E51' },
  bush:       { label: 'Buisson',       hasColor: true,  defaultColor: '#5E9E51' },
  flower:     { label: 'Fleur',     hasColor: true,  defaultColor: '#E15B5B' },
  mushroom:   { label: 'Champignon',   hasColor: false },
  pumpkin:    { label: 'Citrouille',    hasColor: false },
  birdhouse:  { label: 'Nichoir',  hasColor: false },
  pinwheel:   { label: 'Moulinet',   hasColor: false },
  gnome:      { label: 'Nain',      hasColor: false },
  pond:       { label: 'Bassin',       hasColor: false },
  rocks:      { label: 'Cailloux',      hasColor: false },
  sun:        { label: 'Transat',  hasColor: false },
  fence:      { label: 'Barrière',      hasColor: false },
  sign:       { label: 'Panneau',       hasColor: false, hasText: true, defaultText: 'bienvenue' },
};
const GARDEN_ORDER = ['mailbox','photo','link','tree','bush','flower','pond','mushroom','pumpkin','birdhouse','pinwheel','gnome','rocks','sun','fence','sign'];

// Generic edit chip
function EditChip({ onClick, children }) {
  return (
    <button onClick={onClick} style={{
      appearance:'none', background:'#FFF8E0', border:'2px solid #5A4A40', color:'#111',
      padding:'6px 10px', borderRadius: 8, fontFamily:'"Fredoka", system-ui',
      fontWeight:600, fontSize: 12, cursor:'pointer', boxShadow:'0 3px 8px rgba(74,59,51,0.24)',
      whiteSpace:'nowrap',
    }}>{children}</button>
  );
}

Object.assign(window, {
  House, Roof, Floor, Ground, Garden, GardenItem,
  Mailbox, Bush, Tree, Flower, Mushroom, Pumpkin, Birdhouse, Pinwheel, Gnome, Pond, Rocks, SunChair, Fence, Sign,
  GardenPhoto, GardenLink,
  NightSky, DaySky, EditChip,
  GARDEN_LIBRARY, GARDEN_ORDER,
});
