// house-sheets.jsx — bottom-sheet editors for rooms, mailbox, add-room palette,
// roof, garden, profile, and the owner's wall-reply composer.
//
// All editors share a glass-on-paper visual recipe: white card with a thick
// black border + offset shadow (cozy/pixel style for the inside of the
// editors), bottom-sheet shell (Sheet) for the slide-in animation. Glyphs
// reuse the same vocabulary as the linktree icons so the two products feel
// related visually.

// ─────────────────────────────────────────────────────────────
// Sheet shell
// ─────────────────────────────────────────────────────────────
function Sheet({ open, onClose, children, height = 'auto', maxHeight = '82%' }) {
  // IMPORTANT: position FIXED, not absolute. The house page document is far
  // taller than the screen (full-height stage + the earth wall zone below),
  // so an absolutely-positioned sheet anchors to the bottom of the whole
  // document instead of the viewport. That caused two bugs: a closed sheet's
  // cream panel peeked up at the very bottom of the page (the "white strip"),
  // and open sheets stacked mid-document with an unreachable close button.
  // Fixed pins the panel to the viewport bottom; when closed, translateY(100%)
  // pushes it fully below the screen regardless of scroll position.
  return (
    <>
      <div onClick={onClose} style={{
        position:'fixed', inset:0, background: open ? 'rgba(0,0,0,0.35)' : 'transparent',
        transition:'background 0.25s', zIndex: 80, pointerEvents: open ? 'auto' : 'none',
      }}/>
      <div style={{
        position:'fixed', left:0, right:0, bottom:0, zIndex: 81,
        transform: open ? 'translateY(0)' : 'translateY(110%)',
        transition:'transform 0.32s cubic-bezier(.32,.72,.36,1)',
        background:'#FBF6E9', borderTop:'3px solid #111',
        borderRadius:'24px 24px 0 0',
        boxShadow:'0 -12px 32px rgba(0,0,0,0.2)',
        height, maxHeight, display:'flex', flexDirection:'column',
        paddingBottom: 'max(30px, env(safe-area-inset-bottom))',
        pointerEvents: open ? 'auto' : 'none',
      }}>
        <div style={{ width: 44, height: 5, background:'#111', borderRadius: 999,
          margin:'10px auto 4px', opacity: 0.6 }}/>
        <div style={{ flex:1, overflow:'auto', padding:'4px 18px 8px' }}>
          {children}
        </div>
      </div>
    </>
  );
}

// ─────────────────────────────────────────────────────────────
// Room editor — name, wall color, items[]
// ─────────────────────────────────────────────────────────────
function RoomEditor({ room, onClose, onUpdate }) {
  if (!room) return null;
  useHouseLang();
  const meta = ROOM_LIBRARY[room.kind] || ROOM_LIBRARY.empty;
  const [draft, setDraft] = React.useState(() => normalizeRoom(room));
  const [layoutOpen, setLayoutOpen] = React.useState(false);
  React.useEffect(() => setDraft(normalizeRoom(room)), [room?.id]);

  // Live commit: every edit updates local state AND pushes upstream so the
  // change shows on the house immediately and is auto-saved (the network
  // write is debounced in house-app, so rapid edits don't spam the server).
  const commit = (next) => { setDraft(next); onUpdate(next); };

  const setItem = (idx, patch) => commit({
    ...draft, items: draft.items.map((it, i) => i === idx ? { ...it, ...patch } : it)
  });
  const removeItem = (idx) => commit({ ...draft, items: draft.items.filter((_, i) => i !== idx) });
  const addItem = (type) => {
    const id = 'i_' + Math.random().toString(36).slice(2, 8);
    const seeds = {
      photo: { id, type:'photo', frame:'thin', size:'md', seed: 1, caption:'' },
      text:  { id, type:'text',  frame:'tape', size:'md', font:'hand', text:'' },
      link:  { id, type:'link',  size:'md', icon:'web', label:'', url:'' },
    };
    commit({ ...draft, items: [...(draft.items || []), seeds[type]] });
  };

  return (
    <div>
      <SheetHeader
        left={<RoomIcon kind={room.kind} size={26}/>}
        title={roomLabel(room.kind)}
        subtitle={ht('house.editor.subtitle', 'personnalise le mur, donne-lui un nom, accroche ce que tu veux')}
        onClose={onClose}
      />

      <Field label={ht('house.editor.room_name', 'nom de la pièce')}>
        <input type="text" value={draft.title || ''} placeholder={roomLabel(room.kind)}
          onChange={e => commit({ ...draft, title: e.target.value })}
          style={inputStyle}/>
      </Field>

      <Field label={ht('house.editor.wall_color', 'couleur du mur')}>
        <WallSwatches value={draft.wallColor || meta.wall}
          onChange={(c) => commit({ ...draft, wallColor: c })}/>
      </Field>

      <Field label={ht('house.editor.wall_pattern', 'motif du mur')}>
        <PatternChips
          value={draft.wallPattern || meta.pattern || 'plain'}
          wallColor={draft.wallColor || meta.wall}
          onChange={(p) => commit({ ...draft, wallPattern: p })}/>
      </Field>

      <Field label={ht('house.editor.floor', 'sol')}>
        <FloorChips
          value={draft.floorStyle || meta.floorStyle || 'wood'}
          floorColor={draft.floorColor || meta.floor}
          onChange={(s) => commit({ ...draft, floorStyle: s })}/>
      </Field>

      <Field label={ht('house.editor.floor_color', 'couleur du sol')}>
        <FloorSwatches value={draft.floorColor || meta.floor}
          onChange={(c) => commit({ ...draft, floorColor: c })}/>
      </Field>

      <div style={{
        fontFamily:'"JetBrains Mono", monospace', fontSize: 9.5,
        color:'rgba(0,0,0,0.55)', letterSpacing: 0.6, textTransform:'uppercase',
        marginTop: 8, marginBottom: 8,
        display:'flex', alignItems:'center', justifyContent:'space-between',
      }}>
        <span>{ht('house.editor.on_wall', 'au mur ({n}/3)', { n: (draft.items||[]).length })}</span>
      </div>

      {(draft.items || []).length === 0 && (
        <div style={{ background:'#fff8e8', border:'2px dashed #111', borderRadius: 8,
          padding: 14, textAlign:'center',
          fontFamily:'"JetBrains Mono", monospace', fontSize: 11, color:'rgba(0,0,0,0.55)', marginBottom: 10 }}>
          {ht('house.editor.wall_empty', 'mur vide — ajoute une photo, un texte ou un lien')}
        </div>
      )}

      <div style={{ display:'flex', flexDirection:'column', gap: 12 }}>
        {(draft.items || []).map((it, i) => (
          <ItemEditor key={it.id} item={it} idx={i}
            onChange={(patch) => setItem(i, patch)}
            onRemove={() => removeItem(i)}/>
        ))}
      </div>

      {(draft.items || []).length < 3 && (
        <div style={{ display:'flex', gap: 6, marginTop: 12 }}>
          <button onClick={() => addItem('photo')} style={addBtnStyle}>＋ {ht('house.item.photo', 'photo')}</button>
          <button onClick={() => addItem('text')}  style={addBtnStyle}>＋ {ht('house.item.text', 'texte')}</button>
          <button onClick={() => addItem('link')}  style={addBtnStyle}>＋ {ht('house.item.link', 'lien')}</button>
        </div>
      )}

      {(draft.items || []).length > 0 && (
        <button onClick={() => setLayoutOpen(true)}
          style={{ marginTop: 12, width:'100%', appearance:'none', cursor:'pointer',
            background:'#FFE08A', color:'#1c0828', border:'1px solid rgba(74,59,51,0.3)',
            borderRadius: 12, padding:'11px 14px', fontFamily:'"Fredoka", system-ui',
            fontWeight:700, fontSize: 14, boxShadow:'0 2px 8px rgba(74,59,51,0.18)',
            display:'flex', alignItems:'center', justifyContent:'center', gap: 8 }}>
          <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="#1c0828"
               strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M5 3h6v6H5zM14 8l5 5-5 5M3 14v4a2 2 0 002 2h4"/>
          </svg>
          {ht('house.editor.arrange', 'disposer dans la pièce')}
        </button>
      )}

      {layoutOpen && (
        <RoomLayoutEditor
          room={draft}
          onChange={(items) => commit({ ...draft, items })}
          onClose={() => setLayoutOpen(false)}/>
      )}

      <DoneRow onClose={onClose}/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// RoomLayoutEditor — full-screen "arrange the room" surface. The room is
// shown large; each item can be dragged to place, and a corner handle both
// resizes (distance from centre) and rotates (angle around centre). Items may
// overflow the room edges, but a soft resistance near the borders signals
// they're leaving the room. Pointer events unify mouse + touch. Positions are
// stored as percent-of-room (centre) + scale + rotation, matching the render.
// ─────────────────────────────────────────────────────────────
function RoomLayoutEditor({ room, onChange, onClose }) {
  useHouseLang();
  // Local state is the source of truth WHILE arranging. We materialize every
  // item's x/y/scale/rot up front (items that had none get their default
  // layout baked in) so dragging always starts from a concrete value and each
  // change persists instead of being recomputed. We push upstream on every
  // change AND on close.
  const [list, setList] = React.useState(() => {
    const src = room.items || [];
    return src.map((it, i) => ({ ...it, ...withItemLayout(it, i, src.length) }));
  });
  const [sel, setSel] = React.useState(() => (room.items && room.items[0]?.id) || null);
  const boxRef = React.useRef(null);
  const drag = React.useRef(null);
  const listRef = React.useRef(list);
  listRef.current = list;

  // Measure the real on-screen stage width so item sizing matches the house
  // render exactly. baseW uses the SAME fraction of the room width as
  // FreeItemsLayer (ITEM_BASE_FRAC), so an item at a given scale looks the
  // same here as it will in the house — no surprise growth when arranging.
  const [stageW, setStageW] = React.useState(460);
  React.useEffect(() => {
    const measure = () => { if (boxRef.current) setStageW(boxRef.current.getBoundingClientRect().width || 460); };
    measure();
    window.addEventListener('resize', measure);
    return () => window.removeEventListener('resize', measure);
  }, []);

  const ROOM_AR = 1.18; // on-screen room box width/height ratio

  // Patch by id, reading from the live ref (so rapid drags compound correctly),
  // update local state immediately, and push the merged result upstream.
  const patchItem = (id, patch) => {
    const next = listRef.current.map((it) => it.id === id ? { ...it, ...patch } : it);
    listRef.current = next;
    setList(next);
    onChange(next);
  };

  // Soft resistance: past the edge, movement is damped (not blocked) so the
  // user feels the border but can still let items overflow a little.
  const resist = (v) => {
    if (v < 0) return v * 0.45;
    if (v > 100) return 100 + (v - 100) * 0.45;
    return v;
  };

  const onPointerDownMove = (e, it) => {
    e.stopPropagation();
    setSel(it.id);
    const box = boxRef.current.getBoundingClientRect();
    drag.current = { mode:'move', id: it.id, box, startX: e.clientX, startY: e.clientY,
      baseX: (typeof it.x === 'number' ? it.x : 50), baseY: (typeof it.y === 'number' ? it.y : 35) };
    e.currentTarget.setPointerCapture?.(e.pointerId);
  };

  const onPointerDownHandle = (e, it) => {
    e.stopPropagation();
    setSel(it.id);
    const box = boxRef.current.getBoundingClientRect();
    const ix = (typeof it.x === 'number' ? it.x : 50), iy = (typeof it.y === 'number' ? it.y : 35);
    const cx = box.left + (ix / 100) * box.width;
    const cy = box.top + (iy / 100) * box.height;
    const startDist = Math.hypot(e.clientX - cx, e.clientY - cy) || 1;
    const startAng = Math.atan2(e.clientY - cy, e.clientX - cx) * 180 / Math.PI;
    drag.current = { mode:'transform', id: it.id, box, cx, cy, startDist, startAng,
      baseScale: (typeof it.scale === 'number' ? it.scale : 1), baseRot: (typeof it.rot === 'number' ? it.rot : 0) };
    e.currentTarget.setPointerCapture?.(e.pointerId);
  };

  const onPointerMove = (e) => {
    const d = drag.current;
    if (!d) return;
    if (d.mode === 'move') {
      const dxPct = ((e.clientX - d.startX) / d.box.width) * 100;
      const dyPct = ((e.clientY - d.startY) / d.box.height) * 100;
      patchItem(d.id, { x: +resist(d.baseX + dxPct).toFixed(1), y: +resist(d.baseY + dyPct).toFixed(1) });
    } else if (d.mode === 'transform') {
      const dist = Math.hypot(e.clientX - d.cx, e.clientY - d.cy) || 1;
      const ang = Math.atan2(e.clientY - d.cy, e.clientX - d.cx) * 180 / Math.PI;
      const scale = Math.max(0.4, Math.min(3, d.baseScale * (dist / d.startDist)));
      const rot = d.baseRot + (ang - d.startAng);
      patchItem(d.id, { scale: +scale.toFixed(2), rot: +rot.toFixed(0) });
    }
  };
  const onPointerUp = () => { drag.current = null; };

  const baseW = stageW * ITEM_BASE_FRAC; // matches FreeItemsLayer exactly

  return (
    <div style={{ position:'fixed', inset:0, zIndex:120, background:'rgba(28,8,40,0.55)',
      WebkitBackdropFilter:'blur(8px)', backdropFilter:'blur(8px)',
      display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center',
      padding:'16px', boxSizing:'border-box' }}
      onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerUp}>

      <div style={{ width:'100%', maxWidth: 460, display:'flex', alignItems:'center',
        justifyContent:'space-between', marginBottom: 10 }}>
        <div style={{ color:'#fff', fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 16 }}>
          {ht('house.layout.title', 'place tes éléments')}
        </div>
        <button onClick={() => { onChange(listRef.current); onClose(); }} style={{ appearance:'none', cursor:'pointer',
          background:'rgba(255,255,255,0.92)', border:0, borderRadius: 12, padding:'8px 18px',
          fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 13, color:'#1c0828' }}>
          {ht('common.done', 'ok')}
        </button>
      </div>

      <div ref={boxRef} style={{ position:'relative', width:'100%', maxWidth: 460,
        aspectRatio: String(ROOM_AR), borderRadius: 14, overflow:'visible',
        boxShadow:'0 12px 40px rgba(0,0,0,0.4)', touchAction:'none' }}
        onPointerDown={() => setSel(null)}>
        <div style={{ position:'absolute', inset:0, borderRadius: 14, overflow:'hidden' }}>
          <RoomInterior room={{ ...room, items: [] }} w={460} h={460/ROOM_AR}/>
        </div>
        {list.map((it) => {
          const selected = sel === it.id;
          const itemW = baseW * (it.scale || 1);
          return (
            <div key={it.id}
              onPointerDown={(e) => onPointerDownMove(e, it)}
              style={{ position:'absolute', left:`${it.x}%`, top:`${it.y}%`,
                width: itemW, transform:`translate(-50%,-50%) rotate(${it.rot||0}deg)`,
                transformOrigin:'center center', touchAction:'none', cursor:'grab',
                outline: selected ? '2px dashed rgba(255,255,255,0.9)' : 'none',
                outlineOffset: 4, borderRadius: 6,
                filter:'drop-shadow(0 4px 8px rgba(74,59,51,0.25))' }}>
              <Item item={it} max={{ w: itemW, h: itemW * 1.3 }}/>
              {selected && (
                <div onPointerDown={(e) => onPointerDownHandle(e, it)}
                  style={{ position:'absolute', right: -14, bottom: -14, width: 28, height: 28,
                    borderRadius:'50%', background:'#fff', border:'2px solid #1c0828',
                    boxShadow:'0 2px 6px rgba(0,0,0,0.3)', cursor:'nwse-resize',
                    display:'flex', alignItems:'center', justifyContent:'center', touchAction:'none' }}>
                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="#1c0828"
                       strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M21 3l-6 6M3 21l6-6M14 4h6v6M10 20H4v-6"/>
                  </svg>
                </div>
              )}
            </div>
          );
        })}
      </div>

      <div style={{ color:'rgba(255,255,255,0.85)', fontFamily:'"JetBrains Mono", monospace',
        fontSize: 11, marginTop: 12, textAlign:'center', maxWidth: 460, lineHeight: 1.5 }}>
        {ht('house.layout.hint', 'glisse pour déplacer · poignée pour agrandir et pivoter')}
      </div>
    </div>
  );
}

function normalizeRoom(room) {
  // ensure items array exists
  if (!room.items) {
    return { ...room, items: defaultItemsFor(room.kind) };
  }
  return { ...room };
}

// ─────────────────────────────────────────────────────────────
// Item editor — one item's controls (type-specific fields)
// ─────────────────────────────────────────────────────────────
function ItemEditor({ item, idx, onChange, onRemove }) {
  const tlabel = { photo: ht('house.item.photo', 'photo'), text: ht('house.item.text', 'texte'), link: ht('house.item.link', 'lien') }[item.type] || ht('house.item.generic', 'objet');
  return (
    <div style={{ background:'#fff', border:'2.5px solid #111', borderRadius: 10,
      padding:'10px 12px', boxShadow:'3px 3px 0 #111', position:'relative' }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom: 8 }}>
        <div style={{ display:'flex', alignItems:'center', gap: 6 }}>
          <div style={{ width: 18, height: 18, borderRadius:'50%', background:'#FFE08A',
            border:'1.5px solid #111', display:'flex', alignItems:'center', justifyContent:'center',
            fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize:10 }}>{idx + 1}</div>
          <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight:600, fontSize: 13,
            textTransform:'capitalize' }}>{tlabel}</div>
        </div>
        <button onClick={onRemove} style={{
          appearance:'none', width: 24, height: 24, borderRadius: 6,
          border:'2px solid #111', background:'#fff', color:'#111', cursor:'pointer',
          fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize:13, padding:0, lineHeight:1,
          boxShadow:'1.5px 1.5px 0 #111' }}>×</button>
      </div>

      {/* type-specific fields */}
      {item.type === 'photo' && <PhotoFields item={item} onChange={onChange}/>}
      {item.type === 'text'  && <TextFields  item={item} onChange={onChange}/>}
      {item.type === 'link'  && <LinkFields  item={item} onChange={onChange}/>}
    </div>
  );
}

function PhotoFields({ item, onChange }) {
  return (
    <>
      <MiniField label={ht('house.field.palette', 'palette')}>
        <div style={{ display:'flex', gap: 4 }}>
          {[1,2,3,4,5,6].map(s => (
            <button key={s} onClick={() => onChange({ seed: s })}
              style={{ flex:1, height: 40, padding: 0, cursor:'pointer', overflow:'hidden',
                border:`2px solid ${item.seed === s ? '#111' : '#aaa'}`,
                borderRadius: 4, background:'#fff',
                boxShadow: item.seed === s ? '1.5px 1.5px 0 #111' : 'none' }}>
              <FakePhoto seed={s}/>
            </button>
          ))}
        </div>
      </MiniField>
      <MiniField label={ht('house.field.image', 'image (galerie ou url, optionnel)')}>
        <ImagePickerField value={item.src || ''}
          onChange={(src) => onChange({ src })}
          placeholder={ht('house.field.image_placeholder', 'https://… ou importe depuis la galerie')}/>
      </MiniField>
      <MiniField label={ht('house.field.caption', 'légende (optionnel)')}>
        <input type="text" value={item.caption || ''} placeholder={ht('house.field.caption_placeholder', 'ex. soirée pâtes')}
          onChange={e => onChange({ caption: e.target.value })} style={miniInputStyle}/>
      </MiniField>
      <FrameSizeRow item={item} onChange={onChange}/>
      <MiniField label={ht('house.field.link_optional', 'lien (optionnel — rend cliquable)')}>
        <input type="text" value={item.url || ''} placeholder="https://…"
          onChange={e => onChange({ url: e.target.value })} style={miniInputStyle}/>
      </MiniField>
      <PreviewStrip item={item}/>
    </>
  );
}

function TextFields({ item, onChange }) {
  return (
    <>
      <MiniField label={ht('house.field.text', 'texte')}>
        <textarea value={item.text || ''} onChange={e => onChange({ text: e.target.value })}
          placeholder={ht('house.field.text_placeholder', 'ce qui te passe par la tête…')} rows={2}
          style={{ ...miniInputStyle, padding: 8, fontFamily:'"Caveat", cursive',
            fontSize: 16, lineHeight: 1.15, resize:'none' }}/>
      </MiniField>
      <MiniField label={ht('house.field.font', 'police')}>
        <SegRow value={item.font || 'hand'} options={FONT_STYLES}
          onChange={(v) => onChange({ font: v })}/>
      </MiniField>
      <FrameSizeRow item={item} onChange={onChange}/>
      <PreviewStrip item={item}/>
    </>
  );
}

function LinkFields({ item, onChange }) {
  return (
    <>
      <MiniField label={ht('house.field.icon', 'icône')}>
        <div style={{ display:'flex', gap: 4, flexWrap:'wrap' }}>
          {LINK_ICONS_LIST.map(k => {
            const meta = LINK_ICONS_META[k];
            return (
              <button key={k} onClick={() => onChange({ icon: k })} style={{
                width: 32, height: 32, padding: 0, cursor:'pointer',
                background: meta.bg, border: `2px solid ${item.icon === k ? '#111' : 'rgba(0,0,0,0.2)'}`,
                borderRadius: '50%',
                boxShadow: item.icon === k ? '1.5px 1.5px 0 #111' : 'none',
                display:'flex', alignItems:'center', justifyContent:'center',
              }}>
                <LinkIcon kind={k} color={meta.fg} size={18}/>
              </button>
            );
          })}
        </div>
      </MiniField>
      <MiniField label={ht('house.field.label', 'libellé')}>
        <input type="text" value={item.label || ''} placeholder={ht('house.field.label_placeholder', 'ex. Sunday Slow')}
          onChange={e => onChange({ label: e.target.value })} style={miniInputStyle}/>
      </MiniField>
      <MiniField label={ht('house.field.url', 'url')}>
        <input type="text" value={item.url || ''} placeholder="https://…"
          onChange={e => onChange({ url: e.target.value })} style={miniInputStyle}/>
      </MiniField>
      <MiniField label={ht('house.field.size', 'taille')}>
        <SegRow value={item.size || 'md'} options={['sm','md','lg']}
          onChange={(v) => onChange({ size: v })}/>
      </MiniField>
      <PreviewStrip item={item}/>
    </>
  );
}

function FrameSizeRow({ item, onChange }) {
  return (
    <>
      <MiniField label={ht('house.field.frame', 'cadre')}>
        <div style={{ display:'flex', gap: 4, flexWrap:'wrap' }}>
          {FRAME_STYLES.map(fs => (
            <button key={fs} onClick={() => onChange({ frame: fs })}
              style={frameChipStyle(item.frame === fs)}>{ht('house.frame.' + fs, fs)}</button>
          ))}
        </div>
      </MiniField>
      <MiniField label={ht('house.field.size', 'taille')}>
        <SegRow value={item.size || 'md'} options={['sm','md','lg']}
          onChange={(v) => onChange({ size: v })}/>
      </MiniField>
    </>
  );
}

// Live preview of the item at hangin'-on-the-wall scale
function PreviewStrip({ item }) {
  return (
    <div style={{ marginTop: 8, padding: 12, background:'#F1ECDF',
      borderRadius: 6, border:'1.5px dashed rgba(0,0,0,0.25)',
      display:'flex', alignItems:'center', justifyContent:'center', minHeight: 80 }}>
      <Item item={item} max={{ w: 130, h: 70 }}/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Wall color picker
// ─────────────────────────────────────────────────────────────
function WallSwatches({ value, onChange }) {
  return (
    <div style={{ display:'grid', gridTemplateColumns:'repeat(8, 1fr)', gap: 5 }}>
      {WALL_PALETTE.map(c => (
        <button key={c} onClick={() => onChange(c)}
          style={{ appearance:'none', height: 28, padding: 0, cursor:'pointer',
            background: c, border: `2px solid ${value === c ? '#111' : 'rgba(0,0,0,0.25)'}`,
            borderRadius: 6,
            boxShadow: value === c ? '1.5px 1.5px 0 #111' : 'none' }}
          aria-label={c}/>
      ))}
      <label style={{ height: 28, padding: 0, cursor:'pointer',
        background: '#fff', border: '2px dashed #111', borderRadius: 6,
        display:'flex', alignItems:'center', justifyContent:'center',
        fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 14, color:'#111',
        position:'relative', overflow:'hidden' }}>
        +
        <input type="color" value={value} onChange={e => onChange(e.target.value)}
          style={{ position:'absolute', inset:0, opacity:0, cursor:'pointer' }}/>
      </label>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Wall-pattern picker — small swatches previewing each pattern over the
// current wall colour, so the choice is visual rather than a word list.
// ─────────────────────────────────────────────────────────────
const PATTERN_LABELS = { plain:'uni', stripe:'rayé', dots:'points', grid:'grille', tile:'carreaux' };
function PatternChips({ value, wallColor, onChange }) {
  const patterns = window.WALL_PATTERNS || ['plain','stripe','dots','grid','tile'];
  const dark = window.isDarkColor ? window.isDarkColor(wallColor) : false;
  return (
    <div style={{ display:'flex', gap: 6, flexWrap:'wrap' }}>
      {patterns.map(p => {
        const img = window.wallPatternCss ? window.wallPatternCss(p, dark) : 'none';
        const size = window.wallPatternSize ? window.wallPatternSize(p) : 'auto';
        const active = value === p;
        return (
          <button key={p} onClick={() => onChange(p)}
            style={{ appearance:'none', cursor:'pointer', padding: 0,
              width: 52, borderRadius: 8, overflow:'hidden',
              border: `2px solid ${active ? '#111' : 'rgba(0,0,0,0.25)'}`,
              boxShadow: active ? '1.5px 1.5px 0 #111' : 'none', background:'#fff' }}>
            <div style={{ height: 26, background: wallColor,
              backgroundImage: img === 'none' ? 'none' : img, backgroundSize: size }}/>
            <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 8,
              color:'#111', padding:'2px 0', borderTop:'1.5px solid rgba(0,0,0,0.15)' }}>
              {ht('house.wallpat.' + p, PATTERN_LABELS[p] || p)}
            </div>
          </button>
        );
      })}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Floor-style picker — previews each floor treatment in the current floor
// colour.
// ─────────────────────────────────────────────────────────────
const FLOOR_LABELS = { wood:'parquet', tiles:'carrelage', rug:'tapis', concrete:'béton' };
function floorPreviewCss(style) {
  if (style === 'tiles') return { img:'linear-gradient(0deg, rgba(0,0,0,0.16) 0 1.5px, transparent 1.5px 14px), linear-gradient(90deg, rgba(0,0,0,0.16) 0 1.5px, transparent 1.5px 14px)', size:'14px 14px' };
  if (style === 'rug')   return { img:'repeating-linear-gradient(90deg, rgba(255,255,255,0.10) 0 6px, rgba(0,0,0,0.06) 6px 12px)', size:'auto' };
  if (style === 'concrete') return { img:'radial-gradient(rgba(0,0,0,0.08) 1px, transparent 1.5px)', size:'7px 7px' };
  return { img:'repeating-linear-gradient(90deg, rgba(0,0,0,0.10) 0 1px, transparent 1px 12px)', size:'auto' };
}
function FloorChips({ value, floorColor, onChange }) {
  const styles = window.FLOOR_STYLES || ['wood','tiles','rug','concrete'];
  return (
    <div style={{ display:'flex', gap: 6, flexWrap:'wrap' }}>
      {styles.map(s => {
        const prev = floorPreviewCss(s);
        const active = value === s;
        return (
          <button key={s} onClick={() => onChange(s)}
            style={{ appearance:'none', cursor:'pointer', padding: 0,
              width: 60, borderRadius: 8, overflow:'hidden',
              border: `2px solid ${active ? '#111' : 'rgba(0,0,0,0.25)'}`,
              boxShadow: active ? '1.5px 1.5px 0 #111' : 'none', background:'#fff' }}>
            <div style={{ height: 26, background: floorColor,
              backgroundImage: prev.img, backgroundSize: prev.size }}/>
            <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 8,
              color:'#111', padding:'2px 0', borderTop:'1.5px solid rgba(0,0,0,0.15)' }}>
              {ht('house.floor.' + s, FLOOR_LABELS[s] || s)}
            </div>
          </button>
        );
      })}
    </div>
  );
}

// Floor-color picker — deeper, woodier tones suited to floors.
const FLOOR_PALETTE = [
  '#D9A055','#A87648','#82603A','#9E8060','#B36A48',
  '#7A4566','#544B85','#5C8FA3','#4D6B41','#5B7388',
  '#945454','#6B4A2E','#3C3530','#8C8C8C','#2B2B2B',
];
function FloorSwatches({ value, onChange }) {
  return (
    <div style={{ display:'grid', gridTemplateColumns:'repeat(8, 1fr)', gap: 5 }}>
      {FLOOR_PALETTE.map(c => (
        <button key={c} onClick={() => onChange(c)}
          style={{ appearance:'none', height: 28, padding: 0, cursor:'pointer',
            background: c, border: `2px solid ${value === c ? '#111' : 'rgba(0,0,0,0.25)'}`,
            borderRadius: 6, boxShadow: value === c ? '1.5px 1.5px 0 #111' : 'none' }}
          aria-label={c}/>
      ))}
      <label style={{ height: 28, padding: 0, cursor:'pointer',
        background: '#fff', border: '2px dashed #111', borderRadius: 6,
        display:'flex', alignItems:'center', justifyContent:'center',
        fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 14, color:'#111',
        position:'relative', overflow:'hidden' }}>
        +
        <input type="color" value={value} onChange={e => onChange(e.target.value)}
          style={{ position:'absolute', inset:0, opacity:0, cursor:'pointer' }}/>
      </label>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Room palette — pick a kind, drop into the house
// ─────────────────────────────────────────────────────────────
function AddRoomSheet({ onClose, onAdd, mode, slot }) {
  useHouseLang();
  const isFloor = mode === 'floor';
  const title = isFloor ? ht('house.add.title_floor', 'Nouvel étage')
    : (slot ? ht('house.add.title_fill', 'Remplir ce mur') : ht('house.add.title', 'Ajouter une pièce'));
  return (
    <div>
      <SheetHeader
        title={title}
        subtitle={isFloor
          ? ht('house.add.subtitle_floor', 'choisis la première pièce du nouvel étage')
          : ht('house.add.subtitle', 'chaque pièce vient avec son mobilier — le mur, c\'est à toi de le remplir')}
        onClose={onClose}
      />
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap: 10, marginTop: 8 }}>
        {ROOM_ORDER.map(kind => {
          const meta = ROOM_LIBRARY[kind];
          return (
            <button key={kind} onClick={() => { onAdd(kind, isFloor); onClose(); }}
              style={{ appearance:'none', display:'flex', gap: 10, alignItems:'center',
                background: meta.wall, border:'2.5px solid #111', borderRadius: 10,
                padding:'10px 12px', cursor:'pointer', boxShadow:'3px 3px 0 #111', textAlign:'left' }}>
              <div style={{ width: 36, height: 36, background:'#fff', border:'2px solid #111',
                borderRadius: 6, display:'flex', alignItems:'center', justifyContent:'center' }}>
                <RoomIcon kind={kind} size={22}/>
              </div>
              <div style={{ minWidth: 0, flex: 1 }}>
                <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 14, color:'#111',
                  whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{roomLabel(kind)}</div>
                <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 9, color:'rgba(0,0,0,0.55)', letterSpacing:0.3,
                  whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>
                  {kind === 'empty' ? ht('house.add.empty_desc', 'vide — à toi de jouer') : ht('house.add.furnished_desc', 'avec le mobilier {label}', { label: roomLabel(kind).toLowerCase() })}
                </div>
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Mailbox
//
// Real messages from /api/house/me/messages. Each row can be:
//   - marked read (automatic on tap, also explicit button)
//   - deleted
//   - "replied" publicly — opens the wall composer prefilled with the
//     sender's name so the owner can pin a one-line answer to the
//     sidebar without exposing the original letter.
// ─────────────────────────────────────────────────────────────
function MailboxSheet({ messages, onClose, onMarkRead, onDelete, onApprove }) {
  useHouseLang();
  return (
    <div>
      <SheetHeader
        left={<MailGlyph/>}
        title={ht('house.mailbox.title', 'boîte aux lettres')}
        subtitle={ht('house.mailbox.unread', '{n} non-lu(s)', { n: messages.filter(m => !m.read).length })}
        onClose={onClose}
      />
      <div style={{ display:'flex', flexDirection:'column', gap: 10, marginTop: 6 }}>
        {messages.length === 0 && (
          <div style={{ padding: 30, textAlign:'center', fontFamily:'"JetBrains Mono", monospace', fontSize: 11, color:'rgba(0,0,0,0.5)' }}>
            {ht('house.mailbox.empty', 'pas encore de lettre')}
          </div>
        )}
        {messages.map(m => {
          const wantsPublic = m.visibility === 'public';
          const onWall = wantsPublic && m.approved;
          return (
          <div key={m.id} onClick={() => !m.read && onMarkRead?.(m.id)}
            style={{ background:'#fff', border:'2.5px solid #111', borderRadius: 8,
              padding: '10px 12px', boxShadow:'3px 3px 0 #111', cursor: m.read ? 'default' : 'pointer',
              opacity: m.read ? 0.65 : 1, position:'relative' }}>
            {!m.read && <div style={{ position:'absolute', top:8, right:10, width:8, height:8, borderRadius:'50%', background:'#E15B5B' }}/>}
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between' }}>
              <div style={{ display:'flex', alignItems:'center', gap:8 }}>
                <div style={{ width: 22, height: 22, borderRadius:'50%', background: m.color || '#E89D5A',
                  border:'1.5px solid #111', display:'flex', alignItems:'center', justifyContent:'center',
                  fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize:11, color:'#111' }}>
                  {m.from?.[0]?.toUpperCase() || '?'}
                </div>
                <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight:600, fontSize: 13 }}>
                  {m.from}
                  {m.senderId == null && (
                    <span style={{ marginLeft: 6, fontFamily:'"JetBrains Mono", monospace',
                      fontSize: 9, color:'rgba(0,0,0,0.45)', textTransform:'uppercase', letterSpacing: 0.4 }}>{ht('house.mailbox.anon', 'anon')}</span>
                  )}
                </div>
              </div>
              <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 9, color:'rgba(0,0,0,0.45)' }}>
                {formatTime(m.createdAt)}
              </div>
            </div>
            <div style={{ marginTop: 6, fontFamily:'"Caveat", cursive', fontSize: 18, lineHeight: 1.15, color:'#222' }}>
              {m.text}
            </div>

            {/* Status badge: where this message stands in the private→public flow. */}
            <div style={{ marginTop: 8, display:'flex', alignItems:'center', gap: 6 }}>
              {onWall ? (
                <span style={statusBadge('#DDF0D8', '#2E5E2A')}>{ht('house.mailbox.on_wall', 'sur le mur')}</span>
              ) : wantsPublic ? (
                <span style={statusBadge('#FFE9B8', '#7A5A12')}>{ht('house.mailbox.pending', 'veut être public')}</span>
              ) : (
                <span style={statusBadge('#EDE7F5', '#4A3B66')}>{ht('house.mailbox.private', 'privé')}</span>
              )}
            </div>

            <div style={{ marginTop: 8, display:'flex', gap: 6, flexWrap:'wrap' }}>
              {!m.read && (
                <button onClick={(e) => { e.stopPropagation(); onMarkRead?.(m.id); }}
                  style={smallBtnStyle('#FFF8E0')}>{ht('house.mailbox.mark_read', 'marquer lu')}</button>
              )}
              {wantsPublic && !onWall && (
                <button onClick={(e) => { e.stopPropagation(); onApprove?.(m.id, true); }}
                  style={smallBtnStyle('#CDEABF')}>{ht('house.mailbox.approve', 'mettre sur le mur')}</button>
              )}
              {onWall && (
                <button onClick={(e) => { e.stopPropagation(); onApprove?.(m.id, false); }}
                  style={smallBtnStyle('#FFE08A')}>{ht('house.mailbox.unapprove', 'retirer du mur')}</button>
              )}
              <button onClick={(e) => {
                e.stopPropagation();
                if (confirm(ht('house.mailbox.delete_confirm', 'Supprimer cette lettre ?'))) onDelete?.(m.id);
              }} style={smallBtnStyle('#FBE0E0')}>{ht('common.delete', 'supprimer')}</button>
            </div>
          </div>
        );})}
      </div>
    </div>
  );
}

function statusBadge(bg, fg) {
  return {
    display: 'inline-block', background: bg, color: fg,
    fontFamily: '"JetBrains Mono", monospace', fontSize: 9.5, fontWeight: 700,
    letterSpacing: 0.4, textTransform: 'uppercase',
    padding: '3px 8px', borderRadius: 20, border: '1.5px solid rgba(0,0,0,0.12)',
  };
}

// Cozy "5 minutes ago" formatter shared by the mailbox + wall list. We
// avoid a heavy date library — the values we render are short enough
// that a hand-rolled helper is simpler and matches the playful tone.
function formatTime(ts) {
  if (!ts) return '';
  // SQLite stores "YYYY-MM-DD HH:MM:SS" in UTC. Make sure JS parses it
  // as UTC by replacing the space with 'T' and appending 'Z'.
  const parsed = new Date(String(ts).replace(' ', 'T') + (String(ts).endsWith('Z') ? '' : 'Z'));
  if (Number.isNaN(parsed.getTime())) return '';
  const diff = Math.max(0, (Date.now() - parsed.getTime()) / 1000);
  if (diff < 60)      return ht('house.time.now', 'à l\'instant');
  if (diff < 3600)    return ht('house.time.min', '{n}min', { n: Math.floor(diff / 60) });
  if (diff < 86400)   return ht('house.time.hour', '{n}h', { n: Math.floor(diff / 3600) });
  if (diff < 604800)  return ht('house.time.day', '{n}j', { n: Math.floor(diff / 86400) });
  return parsed.toLocaleDateString();
}

function smallBtnStyle(bg) {
  return {
    appearance: 'none',
    background: bg,
    border: '2px solid #111',
    borderRadius: 6,
    padding: '4px 8px',
    fontFamily: '"Fredoka", system-ui',
    fontWeight: 600,
    fontSize: 11,
    color: '#111',
    cursor: 'pointer',
    boxShadow: '1.5px 1.5px 0 #111',
  };
}

// ─────────────────────────────────────────────────────────────
// Wall reply composer — owner-only. Pins a short public note to the
// sidebar. Optionally tied to a letter the owner picked from the
// mailbox (we pass the original sender's name so the reply reads
// "to alice — coffee on saturday?" without leaking the letter).
// ─────────────────────────────────────────────────────────────
function WallReplySheet({ replyTo, onClose, onSubmit }) {
  useHouseLang();
  // replyTo is either null (free-standing post) or { id, from }
  const [text, setText] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const trimmed = text.trim();
  const ok = trimmed.length > 0 && trimmed.length <= 280;
  const submit = async () => {
    if (!ok || busy) return;
    setBusy(true);
    try {
      await onSubmit({
        text: trimmed,
        inReplyTo: replyTo?.from || '',
        messageId: replyTo?.id || null,
      });
      onClose();
    } finally {
      setBusy(false);
    }
  };
  return (
    <div>
      <SheetHeader
        title={replyTo ? ht('house.reply.title_to', 'répondre à {who}', { who: replyTo.from }) : ht('house.reply.title', 'épingler une réponse')}
        subtitle={ht('house.reply.subtitle', 'visible publiquement à côté de ta maison')}
        onClose={onClose}
      />
      <Field label={ht('house.reply.field', 'ton message (280 max)')}>
        <textarea
          value={text}
          onChange={(e) => setText(e.target.value.slice(0, 280))}
          placeholder={replyTo ? ht('house.reply.ph_to', 'merci pour la lettre…') : ht('house.reply.ph', 'un mot pour les passants…')}
          rows={3}
          style={{ ...inputStyle, fontFamily:'"Caveat", cursive', fontSize: 18, resize:'vertical' }}
        />
      </Field>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center',
        fontFamily:'"JetBrains Mono", monospace', fontSize: 10, color:'rgba(0,0,0,0.55)' }}>
        <span>{trimmed.length}/280</span>
        {replyTo && <span>{ht('house.reply.pinned_to', 'épinglé en réponse à @{who}', { who: replyTo.from })}</span>}
      </div>
      <div style={{ display:'flex', gap: 10, marginTop: 18 }}>
        <button onClick={onClose} style={{ ...chipStyle(false), flex:1 }}>{ht('common.cancel', 'annuler')}</button>
        <button onClick={submit} disabled={!ok || busy}
          style={{ ...chipStyle(true), flex:1, background:'#111', color:'#FFF8E8',
            opacity: ok && !busy ? 1 : 0.55 }}>
          {busy ? ht('house.reply.sending', 'envoi…') : ht('house.reply.pin', 'épingler')}
        </button>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Profile
// ─────────────────────────────────────────────────────────────
function ProfileSheet({ houseName, signature, roofStyle, onClose, onUpdate }) {
  useHouseLang();
  const [draft, setDraft] = React.useState({ houseName, signature, roofStyle });
  React.useEffect(() => setDraft({ houseName, signature, roofStyle }), [houseName, signature, roofStyle]);
  const commit = (next) => { setDraft(next); onUpdate(next); };
  return (
    <div>
      <SheetHeader title={ht('house.profile.title', 'Ma maison')} subtitle={ht('house.profile.subtitle', 'ce que les autres voient')} onClose={onClose}/>
      <Field label={ht('house.profile.name', 'nom de la maison (sur la pancarte)')}>
        <input type="text" value={draft.houseName} onChange={e => commit({ ...draft, houseName: e.target.value })}
          style={inputStyle}/>
      </Field>
      <Field label={ht('house.profile.signature', 'signature')}>
        <input type="text" value={draft.signature} onChange={e => commit({ ...draft, signature: e.target.value })}
          style={inputStyle} placeholder="moss"/>
      </Field>
      <DoneRow onClose={onClose}/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Roof editor — style, color, pattern, chimney, antenna, door color
// ─────────────────────────────────────────────────────────────
const ROOF_COLORS = ['#FFFFFF','#FDE4B8','#F5C7C7','#C7DAF5','#C7EFDB','#E2CFEF','#3A4D8F','#7B2D2D','#1F3A2A','#111'];
const DOOR_COLORS = ['#A0532B','#7B2D2D','#1F3A2A','#3A4D8F','#FFE08A','#222'];

function RoofSheet({ roofStyle, roofColor, roofPattern, chimney, smoke, antenna, doorColor, onClose, onUpdate }) {
  useHouseLang();
  const [d, setD] = React.useState({ roofStyle, roofColor, roofPattern, chimney, smoke, antenna, doorColor });
  React.useEffect(() => setD({ roofStyle, roofColor, roofPattern, chimney, smoke, antenna, doorColor }),
    [roofStyle, roofColor, roofPattern, chimney, smoke, antenna, doorColor]);
  const update = (patch) => { const next = { ...d, ...patch }; setD(next); onUpdate(next); };
  return (
    <div>
      <SheetHeader title={ht('house.roof.title', 'Toit & porte')} subtitle={ht('house.roof.subtitle', 'dessine la silhouette de ta maison')} onClose={onClose}/>
      <Field label={ht('house.roof.shape', 'forme')}>
        <div style={{ display:'flex', gap:6 }}>
          {['peaked','flat'].map(s => (
            <button key={s} onClick={() => update({ roofStyle: s })}
              style={chipStyle(d.roofStyle === s)}>{ht('house.roof.shape.' + s, s)}</button>
          ))}
        </div>
      </Field>
      <Field label={ht('house.roof.pattern', 'motif')}>
        <div style={{ display:'flex', gap:6, flexWrap:'wrap' }}>
          {['shingles','dots','stripes','zigzag','tiles','plain'].map(p => (
            <button key={p} onClick={() => update({ roofPattern: p })}
              style={frameChipStyle(d.roofPattern === p)}>{ht('house.roof.pattern.' + p, p)}</button>
          ))}
        </div>
      </Field>
      <Field label={ht('house.roof.color', 'couleur du toit')}>
        <div style={{ display:'grid', gridTemplateColumns:'repeat(8, 1fr)', gap: 5 }}>
          {ROOF_COLORS.map(c => (
            <button key={c} onClick={() => update({ roofColor: c })}
              style={{ appearance:'none', height: 28, padding: 0, cursor:'pointer',
                background: c, border: `2px solid ${d.roofColor === c ? '#111' : 'rgba(0,0,0,0.25)'}`,
                borderRadius: 6,
                boxShadow: d.roofColor === c ? '1.5px 1.5px 0 #111' : 'none' }}/>
          ))}
          <label style={{ height: 28, padding: 0, cursor:'pointer',
            background:'#fff', border:'2px dashed #111', borderRadius: 6,
            display:'flex', alignItems:'center', justifyContent:'center',
            fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 14, color:'#111',
            position:'relative', overflow:'hidden' }}>
            +
            <input type="color" value={d.roofColor} onChange={e => update({ roofColor: e.target.value })}
              style={{ position:'absolute', inset:0, opacity:0, cursor:'pointer' }}/>
          </label>
        </div>
      </Field>
      <Field label={ht('house.roof.extras', 'extras')}>
        <div style={{ display:'flex', flexDirection:'column', gap: 6 }}>
          <ToggleRow label={ht('house.roof.chimney', 'cheminée')} value={d.chimney} onChange={(v) => update({ chimney: v })}/>
          {d.chimney && (
            <ToggleRow label={ht('house.roof.smoke', 'fumée')} value={d.smoke} onChange={(v) => update({ smoke: v })}/>
          )}
          <ToggleRow label={ht('house.roof.antenna', 'antenne')} value={d.antenna} onChange={(v) => update({ antenna: v })}/>
        </div>
      </Field>
      <Field label={ht('house.roof.door_color', 'couleur de la porte')}>
        <div style={{ display:'flex', gap: 5 }}>
          {DOOR_COLORS.map(c => (
            <button key={c} onClick={() => update({ doorColor: c })}
              style={{ appearance:'none', flex:1, height: 32, padding: 0, cursor:'pointer',
                background: c, border: `2px solid ${d.doorColor === c ? '#111' : 'rgba(0,0,0,0.3)'}`,
                borderRadius: 6,
                boxShadow: d.doorColor === c ? '1.5px 1.5px 0 #111' : 'none' }}/>
          ))}
          <label style={{ flex:1, height: 32, padding: 0, cursor:'pointer',
            background:'#fff', border:'2px dashed #111', borderRadius: 6,
            display:'flex', alignItems:'center', justifyContent:'center',
            fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize: 16, color:'#111',
            position:'relative', overflow:'hidden' }}>
            +
            <input type="color" value={d.doorColor} onChange={e => update({ doorColor: e.target.value })}
              style={{ position:'absolute', inset:0, opacity:0, cursor:'pointer' }}/>
          </label>
        </div>
      </Field>
      <DoneRow onClose={onClose}/>
    </div>
  );
}

function ToggleRow({ label, value, onChange }) {
  return (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between',
      padding:'8px 12px', background:'#fff', border:'2px solid #111', borderRadius: 8,
      boxShadow:'2px 2px 0 #111' }}>
      <span style={{ fontFamily:'"Fredoka", system-ui', fontWeight:500, fontSize: 13 }}>{label}</span>
      <button onClick={() => onChange(!value)} style={{
        appearance:'none', position:'relative', width: 38, height: 22, borderRadius: 999, padding:0,
        border:'2px solid #111', background: value ? '#7CE38B' : '#fff', cursor:'pointer',
        transition:'background 0.15s' }}>
        <span style={{ position:'absolute', top: 1, left: value ? 17 : 1,
          width: 16, height: 16, borderRadius:'50%', background:'#111',
          transition:'left 0.15s' }}/>
      </button>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Garden editor — current items + add palette
// ─────────────────────────────────────────────────────────────
const FLOWER_COLORS = ['#E15B5B','#FFD159','#7BA8D8','#E29BBE','#fff','#A95EA0'];
const TREE_COLORS   = ['#5E9E51','#82A26A','#D6A24E','#B26B6B'];

function GardenSheet({ items, onClose, onUpdate }) {
  useHouseLang();
  const [list, setList] = React.useState(items || []);
  React.useEffect(() => setList(items || []), [items]);

  // Live commit so garden changes show on the lawn immediately + auto-save.
  const commitList = (next) => { setList(next); onUpdate(next); };

  const add = (kind) => {
    const meta = GARDEN_LIBRARY[kind];
    const id = 'g_' + Math.random().toString(36).slice(2, 8);
    const newItem = { id, kind, side: 'auto' };
    if (meta?.hasColor) newItem.color = meta.defaultColor;
    if (meta?.hasText)  newItem.text  = meta.defaultText;
    commitList([...(list || []), newItem]);
  };
  const remove = (id) => commitList((list || []).filter(i => i.id !== id));
  const setItem = (id, patch) => commitList((list || []).map(i => i.id === id ? { ...i, ...patch } : i));

  return (
    <div>
      <SheetHeader title={ht('house.garden.title', 'Jardin')} subtitle={ht('house.garden.subtitle', 'ce qu\'il y a dans ta cour')} onClose={onClose}/>

      <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 9.5,
        color:'rgba(0,0,0,0.55)', letterSpacing: 0.6, textTransform:'uppercase', marginBottom: 6 }}>
        {ht('house.garden.in_garden', 'dans ton jardin ({n})', { n: list.length })}
      </div>
      {list.length === 0 && (
        <div style={{ background:'#fff8e8', border:'2px dashed #111', borderRadius: 8,
          padding: 14, textAlign:'center',
          fontFamily:'"JetBrains Mono", monospace', fontSize: 11, color:'rgba(0,0,0,0.55)', marginBottom: 10 }}>
          {ht('house.garden.empty', 'jardin vide — choisis quelque chose ci-dessous')}
        </div>
      )}
      <div style={{ display:'flex', flexDirection:'column', gap: 8, marginBottom: 14 }}>
        {list.map((it, idx) => (
          <GardenRow key={it.id} item={it} idx={idx}
            onRemove={() => remove(it.id)}
            onChange={(p) => setItem(it.id, p)}/>
        ))}
      </div>

      <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 9.5,
        color:'rgba(0,0,0,0.55)', letterSpacing: 0.6, textTransform:'uppercase', marginBottom: 6 }}>
        {ht('house.garden.add', 'ajouter au jardin')}
      </div>
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap: 8 }}>
        {GARDEN_ORDER.map(kind => {
          const meta = GARDEN_LIBRARY[kind];
          return (
            <button key={kind} onClick={() => add(kind)}
              style={{ appearance:'none', display:'flex', flexDirection:'column', alignItems:'center',
                gap: 4, background:'#fff', border:'2px solid #111', borderRadius: 8,
                padding:'8px 4px', cursor:'pointer', boxShadow:'2px 2px 0 #111' }}>
              <div style={{ height: 38, display:'flex', alignItems:'flex-end', justifyContent:'center' }}>
                <GardenItem item={{ kind, color: meta.defaultColor }} night={false}/>
              </div>
              <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight:600, fontSize: 11, color:'#111' }}>{ht('house.garden.kind.' + kind, meta.label)}</div>
            </button>
          );
        })}
      </div>
      <DoneRow onClose={onClose}/>
    </div>
  );
}

function GardenRow({ item, idx, onRemove, onChange }) {
  const meta = GARDEN_LIBRARY[item.kind] || {};
  const colors = item.kind === 'flower' ? FLOWER_COLORS
    : item.kind === 'tree' ? TREE_COLORS
    : ['#E89D5A','#7BA8D8','#A95EA0','#FFD159','#5E9E51','#222'];
  return (
    <div style={{ background:'#fff', border:'2px solid #111', borderRadius: 8,
      padding:'8px 10px', boxShadow:'2px 2px 0 #111', display:'flex', alignItems:'center', gap: 10 }}>
      <div style={{ width: 38, height: 38, display:'flex', alignItems:'flex-end', justifyContent:'center' }}>
        <GardenItem item={item} night={false}/>
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight:600, fontSize: 13 }}>{ht('house.garden.kind.' + item.kind, meta.label || item.kind)}</div>
        {/* Side picker */}
        <div style={{ display:'flex', gap: 3, marginTop: 4 }}>
          {['left','auto','right'].map(s => (
            <button key={s} onClick={() => onChange({ side: s })}
              style={{ appearance:'none', flex:1, padding:'3px 4px',
                background: item.side === s ? '#111' : '#fff', color: item.side === s ? '#fff' : '#111',
                border:'1.5px solid #111', borderRadius: 4,
                fontFamily:'"JetBrains Mono", monospace', fontSize: 9, cursor:'pointer' }}>
              {{ left: ht('house.garden.side.left', 'gauche'), auto: ht('house.garden.side.auto', 'auto'), right: ht('house.garden.side.right', 'droite') }[s]}
            </button>
          ))}
        </div>
        {/* Color picker for items that support it */}
        {meta.hasColor && (
          <div style={{ display:'flex', gap: 3, marginTop: 4 }}>
            {colors.map(c => (
              <button key={c} onClick={() => onChange({ color: c })}
                style={{ appearance:'none', flex:1, height: 16, padding: 0,
                  background: c, border: `1.5px solid ${item.color === c ? '#111' : 'rgba(0,0,0,0.3)'}`,
                  borderRadius: 4, cursor:'pointer' }}/>
            ))}
          </div>
        )}
        {/* Text field for sign */}
        {meta.hasText && (
          <input type="text" value={item.text || ''} onChange={e => onChange({ text: e.target.value })}
            placeholder={meta.defaultText} style={{ ...miniInputStyle, marginTop: 4 }}/>
        )}
        {/* Photo: image (galerie ou URL) + caption — shown to visitors as a popover on tap */}
        {meta.hasPhoto && (
          <>
            <div style={{ marginTop: 4 }}>
              <ImagePickerField value={item.src || ''}
                onChange={(src) => onChange({ src })}
                placeholder={ht('house.garden.photo_ph', 'url de l\'image ou galerie')}/>
            </div>
            <input type="text" value={item.caption || ''} onChange={e => onChange({ caption: e.target.value })}
              placeholder={ht('house.field.caption', 'légende (optionnel)')} maxLength={80} style={{ ...miniInputStyle, marginTop: 4 }}/>
          </>
        )}
        {/* Link: URL + label — opens in a new tab when a visitor taps it */}
        {meta.hasLink && (
          <>
            <input type="url" value={item.url || ''} onChange={e => onChange({ url: e.target.value })}
              placeholder="https://…" style={{ ...miniInputStyle, marginTop: 4 }}/>
            <input type="text" value={item.label || ''} onChange={e => onChange({ label: e.target.value })}
              placeholder={ht('house.garden.sign_ph', 'texte du panneau')} maxLength={40} style={{ ...miniInputStyle, marginTop: 4 }}/>
          </>
        )}
      </div>
      <button onClick={onRemove} style={{
        appearance:'none', width: 22, height: 22, borderRadius: 6,
        border:'2px solid #111', background:'#fff', color:'#111', cursor:'pointer',
        fontFamily:'"Fredoka", system-ui', fontWeight:700, fontSize:12, padding:0, lineHeight:1,
        boxShadow:'1.5px 1.5px 0 #111' }}>×</button>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Shared bits
// ─────────────────────────────────────────────────────────────
function SheetHeader({ title, subtitle, left, onClose }) {
  return (
    <div style={{ display:'flex', gap: 12, alignItems:'flex-start',
      padding:'4px 0 14px', borderBottom:'2px dashed rgba(0,0,0,0.18)', marginBottom: 14 }}>
      {left && <div style={{ width: 30, display:'flex', justifyContent:'center', alignItems:'center' }}>{left}</div>}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight: 700, fontSize: 20, color:'#111', lineHeight: 1.1 }}>{title}</div>
        {subtitle && <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 10,
          color:'rgba(0,0,0,0.55)', marginTop: 4 }}>{subtitle}</div>}
      </div>
      <button onClick={onClose} style={{
        appearance:'none', width: 32, height: 32, borderRadius: '50%', border:'2px solid #111',
        background:'#fff', color:'#111', fontSize: 16, fontWeight: 700, cursor:'pointer', padding: 0,
        boxShadow:'2px 2px 0 #111', lineHeight: 1,
      }}>×</button>
    </div>
  );
}
function Field({ label, children }) {
  return (
    <div style={{ marginBottom: 14 }}>
      <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 9.5,
        color:'rgba(0,0,0,0.55)', letterSpacing: 0.6, textTransform:'uppercase', marginBottom: 6 }}>{label}</div>
      {children}
    </div>
  );
}
function MiniField({ label, children }) {
  return (
    <div style={{ marginBottom: 8 }}>
      <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 8.5,
        color:'rgba(0,0,0,0.5)', letterSpacing: 0.5, textTransform:'uppercase', marginBottom: 4 }}>{label}</div>
      {children}
    </div>
  );
}
function SegRow({ value, options, onChange }) {
  return (
    <div style={{ display:'flex', gap: 4 }}>
      {options.map(o => (
        <button key={o} onClick={() => onChange(o)} style={chipStyle(value === o, { fontSize: 11, padding:'6px 8px' })}>
          {o}
        </button>
      ))}
    </div>
  );
}
const inputStyle = {
  width:'100%', boxSizing:'border-box', background:'#fff', border:'2.5px solid #111',
  borderRadius: 8, padding:'10px 12px', fontFamily:'"Fredoka", system-ui', fontWeight: 500, fontSize: 14, color:'#111',
  outline:'none',
};
const miniInputStyle = {
  width:'100%', boxSizing:'border-box', background:'#FBF6E9', border:'1.5px solid rgba(0,0,0,0.4)',
  borderRadius: 6, padding:'6px 8px', fontFamily:'"Fredoka", system-ui', fontWeight: 500, fontSize: 12, color:'#111',
  outline:'none',
};
const chipStyle = (active, overrides = {}) => ({
  appearance:'none', flex:1, background: active ? '#111' : '#fff', color: active ? '#FFF8E8' : '#111',
  border:'2.5px solid #111', borderRadius: 8, padding:'9px 10px',
  fontFamily:'"Fredoka", system-ui', fontWeight: 600, fontSize: 13, cursor:'pointer',
  boxShadow: active ? 'inset 2px 2px 0 rgba(255,255,255,0.08)' : '2.5px 2.5px 0 #111',
  textTransform:'lowercase',
  ...overrides,
});
const frameChipStyle = (active) => ({
  appearance:'none', background: active ? '#111' : '#fff', color: active ? '#FFF8E8' : '#111',
  border:'2px solid #111', borderRadius: 6, padding:'5px 9px',
  fontFamily:'"JetBrains Mono", monospace', fontWeight: 500, fontSize: 10, cursor:'pointer',
  boxShadow: active ? 'none' : '1.5px 1.5px 0 #111',
  textTransform:'lowercase',
});
const addBtnStyle = {
  appearance:'none', flex:1, background:'#fff', color:'#111',
  border:'2px dashed #111', borderRadius: 8, padding:'10px 6px',
  fontFamily:'"Fredoka", system-ui', fontWeight: 600, fontSize: 12, cursor:'pointer',
};

// ─────────────────────────────────────────────────────────────
// Image picker — a URL field PLUS an "import depuis la galerie" button.
// The URL input keeps working exactly as before (paste a link), so nothing
// is lost; the button just adds a second way in. It opens the device picker
// (accept="image/*", so phones offer the gallery AND the camera), uploads
// the chosen file to /api/upload, and feeds the returned /uploads/... URL
// back through onChange. Used by room photos and garden photos so neither
// is URL-only anymore. Holds its own busy/error state + a tiny preview.
// ─────────────────────────────────────────────────────────────
function ImagePickerField({ value, onChange, placeholder }) {
  const inputRef = React.useRef(null);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');

  const pick = () => { if (inputRef.current) inputRef.current.click(); };

  const onFile = async (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = ''; // let the same file be re-picked later
    if (!file) return;
    setErr('');
    setBusy(true);
    try {
      const fd = new FormData();
      fd.append('file', file);
      const r = await fetch('/api/upload', {
        method: 'POST', credentials: 'same-origin', body: fd,
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || 'upload_failed');
      onChange(j.url);
    } catch (ex) {
      setErr(ex.message === 'too_large'
        ? ht('house.upload.too_large', 'fichier trop lourd (max 8 Mo)')
        : ht('house.upload.failed', 'import impossible — réessaie'));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div>
      <div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}>
        <input type="text" value={value || ''}
          placeholder={placeholder || ht('house.upload.url_ph', 'url de l\'image')}
          onChange={(e) => onChange(e.target.value)}
          style={{ ...miniInputStyle, flex: 1 }}/>
        <button type="button" onClick={pick} disabled={busy}
          style={{
            appearance: 'none', flex: '0 0 auto', whiteSpace: 'nowrap',
            background: '#FFE08A', color: '#111', border: '2px solid #111',
            borderRadius: 6, padding: '0 10px', cursor: busy ? 'default' : 'pointer',
            fontFamily: '"Fredoka", system-ui', fontWeight: 600, fontSize: 11,
            boxShadow: '1.5px 1.5px 0 #111', opacity: busy ? 0.7 : 1,
          }}>
          {busy ? ht('house.upload.sending', 'envoi…') : ht('house.upload.gallery', 'galerie')}
        </button>
        <input ref={inputRef} type="file" accept="image/*"
          style={{ display: 'none' }} onChange={onFile}/>
      </div>
      {value && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 }}>
          <div style={{
            width: 40, height: 40, borderRadius: 6, overflow: 'hidden',
            border: '1.5px solid #111', background: '#EADFCE', flex: '0 0 auto',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <img src={value} alt="" style={{ width: '100%', height: '100%',
              objectFit: 'cover', display: 'block' }}/>
          </div>
          <button type="button" onClick={() => onChange('')}
            style={{ ...smallBtnStyle('#FBE0E0'), fontSize: 10 }}>{ht('house.upload.remove', 'retirer l\'image')}</button>
        </div>
      )}
      {err && (
        <div style={{ marginTop: 6, fontFamily: '"JetBrains Mono", monospace',
          fontSize: 10, color: '#b41e32' }}>{err}</div>
      )}
    </div>
  );
}

// Since every editor now commits live (auto-save), the footer is a single
// "done" button that just closes the sheet — there's nothing to "save".
function DoneRow({ onClose }) {
  return (
    <div style={{ display:'flex', marginTop: 18 }}>
      <button onClick={onClose}
        style={{ ...chipStyle(true), flex:1, background:'#111', color:'#FFF8E8' }}>
        {ht('house.done', 'terminé')}
      </button>
    </div>
  );
}

function MailGlyph() {
  return (
    <svg viewBox="0 0 24 24" width="26" height="26">
      <rect x="3" y="6" width="18" height="12" rx="1.5" fill="#fff" stroke="#111" strokeWidth="2"/>
      <path d="M3 7l9 7 9-7" fill="none" stroke="#111" strokeWidth="2" strokeLinejoin="round"/>
    </svg>
  );
}

// ─────────────────────────────────────────────────────────────
// Room viewer — READ ONLY. This is what a visitor (anyone who is not the
// owner) sees when they tap a room. No inputs, no save: just the wall's
// content laid out nicely. Link items and photos with a URL are clickable
// so the room can still act as a set of links — but nothing here mutates
// the house. The owner gets RoomEditor instead (wired in house-app.jsx).
// ─────────────────────────────────────────────────────────────
const HOUSE_VIEW_FONTS = {
  hand:    '"Caveat", cursive',
  mono:    '"JetBrains Mono", monospace',
  serif:   'Georgia, "Times New Roman", serif',
  display: '"Fredoka", system-ui, sans-serif',
};

function RoomViewer({ room, onClose, reactions, onReact }) {
  if (!room) return null;
  useHouseLang();
  const meta = (window.ROOM_LIBRARY && window.ROOM_LIBRARY[room.kind])
    || (window.ROOM_LIBRARY && window.ROOM_LIBRARY.empty)
    || { label: 'Pièce' };
  const items = Array.isArray(room.items) ? room.items : [];
  return (
    <div>
      <SheetHeader
        title={room.title || (window.roomLabel ? window.roomLabel(room.kind) : meta.label)}
        subtitle={ht('house.viewer.subtitle', 'un coin de la maison')}
        onClose={onClose}
      />
      {items.length === 0 && (
        <div style={{ padding: 26, textAlign: 'center',
          fontFamily: '"JetBrains Mono", monospace', fontSize: 11, color: 'rgba(0,0,0,0.5)' }}>
          {ht('house.viewer.empty', 'rien d\'accroché ici pour l\'instant')}
        </div>
      )}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 6 }}>
        {items.map((it) => {
          // Photo — show the image (or a placeholder), caption underneath.
          // Wrap in <a> only when the owner attached a URL.
          if (it.type === 'photo') {
            const inner = (
              <div style={{ background: '#fff', border: '2.5px solid #111', borderRadius: 10,
                padding: 8, boxShadow: '3px 3px 0 #111' }}>
                <div style={{ borderRadius: 6, overflow: 'hidden', background: '#EADFce',
                  aspectRatio: '4 / 3', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  {it.src
                    ? <img src={it.src} alt={it.caption || ''}
                        style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
                    : <span style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: 10,
                        color: 'rgba(0,0,0,0.4)' }}>photo</span>}
                </div>
                {it.caption && (
                  <div style={{ marginTop: 6, fontFamily: '"Caveat", cursive', fontSize: 17,
                    lineHeight: 1.15, color: '#222' }}>{it.caption}</div>
                )}
              </div>
            );
            return it.url
              ? <a key={it.id} href={it.url} target="_blank" rel="noopener noreferrer"
                   style={{ textDecoration: 'none' }}>{inner}</a>
              : <div key={it.id}>{inner}</div>;
          }
          // Text — render in the chosen font/colors, no editing.
          if (it.type === 'text') {
            return (
              <div key={it.id} style={{
                background: it.bg || '#FFF8E8', border: '2.5px solid #111', borderRadius: 10,
                padding: '12px 14px', boxShadow: '3px 3px 0 #111',
                fontFamily: HOUSE_VIEW_FONTS[it.font] || HOUSE_VIEW_FONTS.hand,
                fontSize: it.font === 'hand' ? 20 : 15, lineHeight: 1.25,
                color: it.color || '#1c0828', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
                {it.text || ''}
              </div>
            );
          }
          // Link — clickable chip with icon + label.
          if (it.type === 'link') {
            const LinkIconComp = window.LinkIcon;
            return (
              <a key={it.id} href={it.url || '#'} target="_blank" rel="noopener noreferrer"
                style={{ display: 'flex', alignItems: 'center', gap: 10,
                  background: '#fff', border: '2.5px solid #111', borderRadius: 10,
                  padding: '12px 14px', boxShadow: '3px 3px 0 #111', textDecoration: 'none',
                  color: '#1c0828' }}>
                <div style={{ width: 30, height: 30, borderRadius: 8, background: '#FFE08A',
                  border: '2px solid #111', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  {LinkIconComp ? <LinkIconComp name={it.icon} size={16}/> : null}
                </div>
                <div style={{ flex: 1, minWidth: 0, fontFamily: '"Fredoka", system-ui',
                  fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden',
                  textOverflow: 'ellipsis' }}>
                  {it.label || it.url || 'lien'}
                </div>
                <svg viewBox="0 0 16 16" width="13" height="13" fill="none"
                     stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M5 3l5 5-5 5"/>
                </svg>
              </a>
            );
          }
          return null;
        })}
      </div>
      {onReact && <RoomReactions roomId={room.id} reactions={reactions} onReact={onReact}/>}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// RoomReactions — one-tap emoji reactions on a room. Shows the running
// count per emoji; tapping bumps it (optimistically) and calls onReact.
// A visitor can react once per emoji per room per session (tracked in a
// module-level set) so the UI doesn't invite spam; the server also caps it.
// ─────────────────────────────────────────────────────────────
const REACTION_EMOJIS = ['❤️', '✨', '🔥', '😮', '🌿'];
const reactedThisSession = new Set();
function RoomReactions({ roomId, reactions, onReact }) {
  useHouseLang();
  const [counts, setCounts] = React.useState(reactions || {});
  React.useEffect(() => { setCounts(reactions || {}); }, [roomId]);
  const tap = (emoji) => {
    const key = roomId + '|' + emoji;
    if (reactedThisSession.has(key)) return;
    reactedThisSession.add(key);
    setCounts((c) => ({ ...c, [emoji]: (c[emoji] || 0) + 1 }));
    Promise.resolve(onReact(roomId, emoji))
      .then((fresh) => { if (fresh && typeof fresh === 'object') setCounts(fresh); })
      .catch(() => {});
  };
  return (
    <div style={{ marginTop: 14, paddingTop: 12, borderTop: '2px dashed rgba(0,0,0,0.15)' }}>
      <div style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: 9.5,
        color: 'rgba(0,0,0,0.5)', letterSpacing: 0.5, textTransform: 'uppercase', marginBottom: 8 }}>
        {ht('house.react.prompt', 'une réaction ?')}
      </div>
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
        {REACTION_EMOJIS.map((e) => {
          const n = counts[e] || 0;
          const done = reactedThisSession.has(roomId + '|' + e);
          return (
            <button key={e} onClick={() => tap(e)} disabled={done}
              style={{ appearance: 'none', display: 'flex', alignItems: 'center', gap: 5,
                background: done ? '#FFE08A' : '#fff', border: '2px solid #111', borderRadius: 999,
                padding: '5px 10px', cursor: done ? 'default' : 'pointer',
                boxShadow: done ? 'none' : '1.5px 1.5px 0 #111',
                transform: done ? 'translate(1.5px,1.5px)' : 'none',
                fontFamily: '"Fredoka", system-ui', fontWeight: 600, fontSize: 13 }}>
              <span style={{ fontSize: 15, lineHeight: 1 }}>{e}</span>
              {n > 0 && <span style={{ fontSize: 11, color: '#1c0828' }}>{n}</span>}
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Add a neighbour directly by pseudo — the simplest path, no need to wait
// for someone to surface in "découvrir". Sends a friend request to the
// exact pseudo and reports back precisely (introuvable, maison fermée,
// déjà voisins, demande envoyée…). The message-based suggestions still
// live in the tabs below; this is just the quick "I already know their
// pseudo" shortcut, which is what most people reach for first.
// ─────────────────────────────────────────────────────────────
function AddFriendByPseudo({ onSendRequest }) {
  useHouseLang();
  const [value, setValue]   = React.useState('');
  const [status, setStatus] = React.useState({ kind: 'idle', msg: '' });
  const [busy, setBusy]     = React.useState(false);

  const submit = async () => {
    const pseudo = value.trim().replace(/^@+/, '');
    if (!pseudo || busy) return;
    setBusy(true);
    setStatus({ kind: 'sending', msg: ht('house.nb.sending', 'envoi…') });
    const res = await onSendRequest(pseudo);
    setBusy(false);
    if (res && res.ok) {
      if (res.status === 'accepted') {
        setStatus({ kind: 'ok', msg: ht('house.nb.now_neighbours', 'vous êtes maintenant voisins.') });
        setValue('');
      } else if (res.status === 'outgoing_pending') {
        setStatus({ kind: 'ok', msg: ht('house.nb.request_sent', 'demande envoyée à @{pseudo}', { pseudo }) });
        setValue('');
      } else if (res.status === 'self') {
        setStatus({ kind: 'err', msg: ht('house.nb.err_self', 'c\'est ta propre maison.') });
      } else if (res.status === 'blocked') {
        setStatus({ kind: 'err', msg: ht('house.nb.err_blocked', 'impossible d\'ajouter cette personne.') });
      } else {
        setStatus({ kind: 'ok', msg: ht('house.nb.done', 'c\'est fait.') });
        setValue('');
      }
    } else {
      const e = res && res.error;
      const msg =
        e === 'not_found'      ? ht('house.nb.err_not_found', 'aucune maison à ce pseudo.') :
        e === 'house_disabled' ? ht('house.nb.err_disabled', 'cette personne n\'a pas encore ouvert sa maison.') :
        e === 'missing_pseudo' ? ht('house.nb.err_missing', 'écris d\'abord un pseudo.') :
        e === 'network'        ? ht('house.nb.err_network', 'erreur réseau — réessaie.') :
                                 ht('house.nb.err_generic', 'impossible d\'ajouter — réessaie.');
      setStatus({ kind: 'err', msg });
    }
  };

  const disabled = busy || !value.trim();
  return (
    <div style={{
      background: '#fff', border: '2px solid #111', borderRadius: 10,
      boxShadow: '2px 2px 0 #111', padding: '10px 12px', marginBottom: 12,
    }}>
      <div style={{ fontFamily: '"Fredoka", system-ui', fontWeight: 600,
        fontSize: 12, marginBottom: 6 }}>
        {ht('house.nb.add_by_pseudo', 'ajouter un ami par pseudo')}
      </div>
      <div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}>
        <div style={{ position: 'relative', flex: 1, display: 'flex', alignItems: 'center' }}>
          <span style={{
            position: 'absolute', left: 8, fontFamily: '"Fredoka", system-ui',
            fontWeight: 600, fontSize: 12, color: 'rgba(0,0,0,0.45)', pointerEvents: 'none',
          }}>@</span>
          <input type="text" value={value} placeholder={ht('house.nb.pseudo_ph', 'pseudo')}
            onChange={(e) => {
              setValue(e.target.value);
              if (status.kind !== 'idle') setStatus({ kind: 'idle', msg: '' });
            }}
            onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } }}
            style={{ ...miniInputStyle, paddingLeft: 18 }}/>
        </div>
        <button type="button" onClick={submit} disabled={disabled}
          style={{
            appearance: 'none', flex: '0 0 auto', whiteSpace: 'nowrap',
            background: disabled ? '#EADFCE' : '#FFE08A', color: '#111',
            border: '2px solid #111', borderRadius: 6, padding: '0 12px',
            cursor: disabled ? 'default' : 'pointer',
            fontFamily: '"Fredoka", system-ui', fontWeight: 600, fontSize: 12,
            boxShadow: '1.5px 1.5px 0 #111',
          }}>
          {ht('house.nb.add', 'ajouter')}
        </button>
      </div>
      {status.kind !== 'idle' && (
        <div style={{
          marginTop: 6, fontFamily: '"JetBrains Mono", monospace', fontSize: 10.5,
          color: status.kind === 'err' ? '#b41e32'
               : status.kind === 'ok'  ? '#2f7d34' : 'rgba(0,0,0,0.55)',
        }}>
          {status.msg}
        </div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Neighbourhood — the little "voisinage" panel
//
// Three tabs for the owner: friends (accepted), requests (incoming +
// outgoing pending), discover (random sample of unconnected houses).
// Visitors only see the friends list (and only when the owner's
// friends_public toggle is on — gating is enforced server-side).
//
// Each friend renders as a tiny clickable mini-house painted in their
// roof + door colours, with their pseudo underneath. Tapping opens
// their house in a new tab so the visitor doesn't lose their place.
// ─────────────────────────────────────────────────────────────
function NeighbourhoodSheet({
  isOwner, ownerPseudo, friendsPublic,
  data,
  discover,
  loading, error,
  onClose,
  onSendRequest, onAccept, onRemove,
  onToggleVisibility, onRefresh,
}) {
  useHouseLang();
  const [tab, setTab] = React.useState('friends');

  if (loading) {
    return (
      <div>
        <SheetHeader title={ht('house.nb.title', 'le voisinage')}
          subtitle={isOwner ? ht('house.nb.subtitle_owner', 'tes amis sur stanmaxx') : ht('house.nb.subtitle_visitor', 'les voisins de @{pseudo}', { pseudo: ownerPseudo })}
          onClose={onClose}/>
        <div style={{ padding: 30, textAlign:'center',
          fontFamily:'"JetBrains Mono", monospace', fontSize: 11, color:'rgba(0,0,0,0.5)' }}>
          {ht('house.nb.loading', 'chargement…')}
        </div>
      </div>
    );
  }

  if (!isOwner) {
    if (data?.visible === false) {
      return (
        <div>
          <SheetHeader title={ht('house.nb.title', 'le voisinage')} subtitle={ht('house.nb.subtitle_visitor', 'les voisins de @{pseudo}', { pseudo: ownerPseudo })} onClose={onClose}/>
          <div style={{ padding: 30, textAlign:'center',
            fontFamily:'"JetBrains Mono", monospace', fontSize: 11, color:'rgba(0,0,0,0.55)' }}>
            {ht('house.nb.private', 'ce voisinage est privé.')}
          </div>
        </div>
      );
    }
    const list = data?.friends || [];
    return (
      <div>
        <SheetHeader title={ht('house.nb.title', 'le voisinage')} subtitle={ht('house.nb.subtitle_visitor', 'les voisins de @{pseudo}', { pseudo: ownerPseudo })} onClose={onClose}/>
        {list.length === 0 && (
          <div style={{ padding: 24, textAlign:'center',
            fontFamily:'"JetBrains Mono", monospace', fontSize: 11, color:'rgba(0,0,0,0.5)' }}>
            {ht('house.nb.no_neighbours', 'pas encore de voisins ici.')}
          </div>
        )}
        <MiniHouseGrid friends={list} centerLabel={'@' + ownerPseudo}/>
      </div>
    );
  }

  const friends  = data?.friends  || [];
  const incoming = data?.incoming || [];
  const outgoing = data?.outgoing || [];
  return (
    <div>
      <SheetHeader title={ht('house.nb.title', 'le voisinage')} subtitle={ht('house.nb.subtitle_owner', 'tes amis sur stanmaxx')} onClose={onClose}/>

      <div style={{
        display:'flex', alignItems:'center', justifyContent:'space-between',
        gap: 10, padding: '8px 12px', background:'#FFF8E0',
        border:'2px solid #111', borderRadius: 10, boxShadow:'2px 2px 0 #111',
        marginBottom: 12,
      }}>
        <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight: 600, fontSize: 12 }}>
          {ht('house.nb.map_visible', 'carte visible des visiteurs')}
          <div style={{ fontWeight: 400, fontSize: 10.5, color:'rgba(0,0,0,0.55)',
            fontFamily:'"JetBrains Mono", monospace', marginTop: 2 }}>
            {friendsPublic ? ht('house.nb.everyone_sees', 'tout le monde voit tes voisins') : ht('house.nb.only_you_see', 'seulement toi vois tes voisins')}
          </div>
        </div>
        <button onClick={() => onToggleVisibility(!friendsPublic)}
          style={{
            appearance:'none', width: 44, height: 26, padding: 2, border:'2px solid #111',
            borderRadius: 999, background: friendsPublic ? '#5E9E51' : '#fff', cursor:'pointer',
            position:'relative', boxShadow:'1.5px 1.5px 0 #111',
          }}>
          <span style={{
            position:'absolute', top: 2, left: friendsPublic ? 22 : 2,
            width: 18, height: 18, background:'#111', borderRadius:'50%',
            transition:'left .15s',
          }}/>
        </button>
      </div>

      <AddFriendByPseudo onSendRequest={onSendRequest}/>

      <div style={{ display:'flex', gap: 6, marginBottom: 10 }}>
        <NeighbourTab active={tab === 'friends'}  onClick={() => setTab('friends')}  label={ht('house.nb.tab_friends', 'amis ({n})', { n: friends.length })}/>
        <NeighbourTab active={tab === 'requests'} onClick={() => setTab('requests')} label={ht('house.nb.tab_requests', 'demandes ({n})', { n: incoming.length })} dot={incoming.length > 0}/>
        <NeighbourTab active={tab === 'discover'} onClick={() => setTab('discover')} label={ht('house.nb.tab_discover', 'découvrir')}/>
      </div>

      {error && (
        <div style={{ padding: 10, marginBottom: 10, background:'#FBE0E0',
          border:'2px solid #111', borderRadius: 8, fontSize: 11,
          fontFamily:'"JetBrains Mono", monospace' }}>
          {ht('house.nb.error', 'erreur — réessaie')}
        </div>
      )}

      {tab === 'friends' && (
        <>
          {friends.length === 0 && (
            <div style={{ padding: 22, textAlign:'center',
              fontFamily:'"JetBrains Mono", monospace', fontSize: 11, color:'rgba(0,0,0,0.5)' }}>
              {ht('house.nb.friends_empty1', 'tu n\'as pas encore d\'amis ici.')}<br/>
              {ht('house.nb.friends_empty2', 'va dans "découvrir" pour trouver des maisons.')}
            </div>
          )}
          <MiniHouseGrid friends={friends} onRemove={onRemove} canRemove/>
        </>
      )}

      {tab === 'requests' && (
        <>
          <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 10,
            color:'rgba(0,0,0,0.55)', letterSpacing: 0.5, textTransform:'uppercase',
            marginBottom: 6 }}>
            {ht('house.nb.they_asked', 'ils t\'ont demandé ({n})', { n: incoming.length })}
          </div>
          {incoming.length === 0 && (
            <div style={{ padding: 14, textAlign:'center', fontSize: 11,
              fontFamily:'"JetBrains Mono", monospace', color:'rgba(0,0,0,0.45)' }}>
              {ht('house.nb.no_requests', 'aucune demande')}
            </div>
          )}
          {incoming.map(f => (
            <RequestRow key={f.userId} friend={f}
              onAccept={() => onAccept(f.pseudo)}
              onRemove={() => onRemove(f.pseudo)}
              kind="incoming"/>
          ))}
          <div style={{ height: 12 }}/>
          <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 10,
            color:'rgba(0,0,0,0.55)', letterSpacing: 0.5, textTransform:'uppercase',
            marginBottom: 6 }}>
            {ht('house.nb.you_asked', 'tu as demandé ({n})', { n: outgoing.length })}
          </div>
          {outgoing.length === 0 && (
            <div style={{ padding: 14, textAlign:'center', fontSize: 11,
              fontFamily:'"JetBrains Mono", monospace', color:'rgba(0,0,0,0.45)' }}>
              {ht('house.nb.nothing_pending', 'rien en attente')}
            </div>
          )}
          {outgoing.map(f => (
            <RequestRow key={f.userId} friend={f}
              onRemove={() => onRemove(f.pseudo)}
              kind="outgoing"/>
          ))}
        </>
      )}

      {tab === 'discover' && (
        <>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 8 }}>
            <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 10,
              color:'rgba(0,0,0,0.55)', letterSpacing: 0.5, textTransform:'uppercase' }}>
              {ht('house.nb.discover_some', 'quelques maisons au hasard')}
            </div>
            <button onClick={onRefresh} style={{
              appearance:'none', background:'#FFF8E0', border:'2px solid #111',
              borderRadius: 6, padding:'3px 8px', fontSize: 10,
              fontFamily:'"Fredoka", system-ui', fontWeight:600, cursor:'pointer',
              boxShadow:'1.5px 1.5px 0 #111',
            }}>{ht('house.nb.discover_more', '↻ autres')}</button>
          </div>
          {(discover || []).length === 0 && (
            <div style={{ padding: 14, textAlign:'center', fontSize: 11,
              fontFamily:'"JetBrains Mono", monospace', color:'rgba(0,0,0,0.45)' }}>
              {ht('house.nb.no_suggestions', 'pas de suggestions pour le moment')}
            </div>
          )}
          {(discover || []).map(f => (
            <RequestRow key={f.userId} friend={f}
              onAccept={() => onSendRequest(f.pseudo)}
              kind="discover"/>
          ))}
        </>
      )}

      <DoneRow onClose={onClose}/>
    </div>
  );
}

function NeighbourTab({ active, onClick, label, dot }) {
  return (
    <button onClick={onClick} style={{
      appearance:'none', position:'relative',
      padding:'6px 10px', borderRadius: 8,
      background: active ? '#111' : '#fff', color: active ? '#FFF8E8' : '#111',
      border:'2px solid #111', boxShadow: active ? 'none' : '2px 2px 0 #111',
      fontFamily:'"Fredoka", system-ui', fontWeight: 600, fontSize: 11, cursor:'pointer',
    }}>
      {label}
      {dot && <span style={{ position:'absolute', top:-3, right:-3, width:8, height:8,
        borderRadius:'50%', background:'#E15B5B', border:'1.5px solid #111' }}/>}
    </button>
  );
}

function MiniHouse({ friend, size = 56 }) {
  const roofColor = friend.roofColor || '#FFFFFF';
  const doorColor = friend.doorColor || '#A0532B';
  // Hard cozy drop-shadow that follows the house silhouette (not a square
  // box like before). Scales with the rendered size so a 32px map marker
  // and a 56px grid tile both read cleanly.
  const sh = Math.max(1, Math.round(size / 28));
  return (
    <div style={{
      width: size, height: size, position: 'relative', flex: '0 0 auto',
      display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
    }}>
      <svg viewBox="0 0 44 44" width={size} height={size}
        style={{ overflow: 'visible',
          filter: `drop-shadow(${sh}px ${sh}px 0 rgba(17,17,17,0.92))` }}>
        {/* walls — warm cream, sat on the ground at y=38 */}
        <rect x="9" y="20" width="26" height="18" rx="1.5"
          fill="#FFF1DA" stroke="#111" strokeWidth="2"/>
        {/* chimney — drawn before the roof so the roof tucks over its base */}
        <rect x="28.5" y="8.5" width="3.6" height="9"
          fill="#C77B53" stroke="#111" strokeWidth="1.6" strokeLinejoin="round"/>
        {/* pitched roof in the friend's roof colour, overhanging the walls */}
        <path d="M5 21 L22 7 L39 21 Z"
          fill={roofColor} stroke="#111" strokeWidth="2" strokeLinejoin="round"/>
        {/* left window with little muntins */}
        <g stroke="#111" strokeWidth="1.4">
          <rect x="11.5" y="24" width="6" height="6" rx="0.8" fill="#BFE3F2"/>
          <line x1="14.5" y1="24" x2="14.5" y2="30"/>
          <line x1="11.5" y1="27" x2="17.5" y2="27"/>
        </g>
        {/* right window */}
        <g stroke="#111" strokeWidth="1.4">
          <rect x="26.5" y="24" width="6" height="6" rx="0.8" fill="#BFE3F2"/>
          <line x1="29.5" y1="24" x2="29.5" y2="30"/>
          <line x1="26.5" y1="27" x2="32.5" y2="27"/>
        </g>
        {/* door in the friend's door colour, with a rounded top + knob */}
        <path d="M19 38 L19 29.5 Q19 28 20.5 28 L23.5 28 Q25 28 25 29.5 L25 38 Z"
          fill={doorColor} stroke="#111" strokeWidth="1.7" strokeLinejoin="round"/>
        <circle cx="23.5" cy="33.2" r="0.9" fill="#111"/>
      </svg>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Village — a tiny map of the neighbourhood.
//
// Instead of a grid of mini-houses, we draw a small SVG village: your
// maison sits in the centre and friends are arranged on a ring around
// it, each connected by a dirt road. Decorative trees, bushes and rocks
// fill the empty space so the map looks like a populated countryside,
// not a placeholder. Each friend's house is painted in their actual
// roof + door colours, and tapping one opens their house.
// ─────────────────────────────────────────────────────────────
function Village({ friends, onRemove, canRemove, ownerPseudo }) {
  const N = (friends || []).length;

  // Empty state: a single tiny field with a sign post — better than an
  // awkward solo "you" house with no roads going anywhere.
  if (N === 0) {
    return (
      <div style={{
        position: 'relative', width: '100%',
        aspectRatio: '4 / 3', borderRadius: 14,
        border: '2.5px solid #111', boxShadow: '3px 3px 0 #111',
        background:
          'radial-gradient(rgba(0,0,0,.05) 1px, transparent 1.4px) 0 0 / 9px 9px, ' +
          'linear-gradient(180deg, #B4D693 0%, #97C076 100%)',
        overflow: 'hidden',
      }}>
        <div style={{
          position: 'absolute', inset: 0, display: 'flex',
          alignItems: 'center', justifyContent: 'center',
          flexDirection: 'column', gap: 6,
        }}>
          <div style={{
            background: '#FFE08A', border: '2.5px solid #111',
            borderRadius: 5, boxShadow: '2px 2px 0 #111',
            padding: '4px 10px', fontFamily: '"Fredoka", system-ui',
            fontWeight: 700, fontSize: 11, color: '#1c0828',
          }}>
            pas encore de voisins
          </div>
          <div style={{ width: 3, height: 22, background: '#111' }}/>
        </div>
        {/* a few decorative trees in the corners */}
        <VillageTree cx={42}  cy={28}  scale={1}/>
        <VillageTree cx={88}  cy={22}  scale={.85}/>
        <VillageBush cx={20}  cy={88}  scale={1}/>
        <VillageRock cx={92}  cy={92}  scale={.9}/>
      </div>
    );
  }

  // Layout: position each friend on a ring around the owner's central
  // house. The ring radius widens slightly with the friend count so the
  // map doesn't get too dense. With 1–2 friends we keep them at the same
  // visual distance for symmetry; from 3+ we lay them out evenly around
  // a circle. Positions are in percent so the SVG scales fluidly with
  // its container.
  const cx = 50, cy = 50; // center, where the owner sits
  const radius =
    N <= 2 ? 30 :
    N <= 5 ? 32 :
    N <= 8 ? 35 : 37;

  // Start angle: top of the circle. We rotate slightly to break the
  // straight up/down symmetry so the map feels organic.
  const startAngle = -Math.PI / 2 + 0.18;
  const positions = friends.map((f, i) => {
    const t = (i / N) * Math.PI * 2 + startAngle;
    return { x: cx + Math.cos(t) * radius, y: cy + Math.sin(t) * radius };
  });

  // Decorative scatter — deterministic so the same village renders
  // identically every time the panel opens. We pick spots that aren't
  // on a road and aren't too close to a house.
  const decorations = useVillageDecorations(positions, N);

  return (
    <div style={{
      position: 'relative', width: '100%',
      aspectRatio: N <= 4 ? '4 / 3' : N <= 8 ? '5 / 4' : '1 / 1',
      borderRadius: 14, border: '2.5px solid #111', boxShadow: '3px 3px 0 #111',
      background:
        'radial-gradient(rgba(0,0,0,.05) 1px, transparent 1.4px) 0 0 / 9px 9px, ' +
        'linear-gradient(180deg, #B4D693 0%, #97C076 100%)',
      overflow: 'hidden',
    }}>
      {/* SVG underlay: dirt roads connecting the centre to every friend,
          plus a soft ring path threading the friends together (gives the
          village its "we know each other" feel without being a literal
          graph). Roads are wide dark earth strokes with a dashed beige
          centre line, matching the road style used on the sidebar. */}
      <svg viewBox="0 0 100 100" preserveAspectRatio="none"
        style={{ position: 'absolute', inset: 0, width: '100%', height: '100%',
          pointerEvents: 'none' }}>
        {/* roads centre → friend */}
        {positions.map((p, i) => (
          <g key={'road-'+i}>
            <line x1={cx} y1={cy} x2={p.x} y2={p.y}
              stroke="#5A4A2E" strokeWidth="5.5" strokeLinecap="round"/>
            <line x1={cx} y1={cy} x2={p.x} y2={p.y}
              stroke="#C7B083" strokeWidth="3.5" strokeLinecap="round"/>
            <line x1={cx} y1={cy} x2={p.x} y2={p.y}
              stroke="#FFF8E0" strokeWidth="0.5" strokeDasharray="1.6 2.2"/>
          </g>
        ))}
        {/* ring road threading friends together (only when 3+ friends so
            the ring actually closes into something recognisable). */}
        {N >= 3 && (
          <>
            <polygon
              points={positions.map(p => `${p.x},${p.y}`).join(' ')}
              fill="none" stroke="#5A4A2E" strokeWidth="3.2" strokeLinejoin="round"/>
            <polygon
              points={positions.map(p => `${p.x},${p.y}`).join(' ')}
              fill="none" stroke="#C7B083" strokeWidth="1.8" strokeLinejoin="round"/>
          </>
        )}
        {/* decorative scatter (trees / bushes / rocks) drawn UNDER the
            houses but OVER the grass + roads */}
        {decorations.map((d, i) => {
          if (d.kind === 'tree')  return <VillageTree key={'d-'+i} cx={d.x} cy={d.y} scale={d.scale}/>;
          if (d.kind === 'bush')  return <VillageBush key={'d-'+i} cx={d.x} cy={d.y} scale={d.scale}/>;
          return <VillageRock key={'d-'+i} cx={d.x} cy={d.y} scale={d.scale}/>;
        })}
      </svg>

      {/* Centre marker: the OWNER's house. Always coloured, with "moi"
          underneath so the user knows which one is theirs. */}
      <VillageMarker
        x={cx} y={cy}
        friend={{ pseudo: ownerPseudo, roofColor: '#FFE08A', doorColor: '#A0532B',
          palette: ['#FFE066','#FF8A3D','#FF3D7F','#9D2BFF'] }}
        label="moi"
        isCenter
      />

      {/* Friend markers */}
      {friends.map((f, i) => (
        <VillageMarker key={f.userId}
          x={positions[i].x} y={positions[i].y}
          friend={f}
          href={`/${f.pseudo}/house`}
          onRemove={canRemove ? () => {
            if (confirm(`Retirer @${f.pseudo} de tes voisins ?`)) onRemove(f.pseudo);
          } : undefined}
        />
      ))}
    </div>
  );
}

// Compute a deterministic list of decorative items. We use a seeded RNG so
// the map looks identical on every open of the sheet (less jittery than
// Math.random) but still varied enough between users. We also skip spots
// too close to a house or to a road.
function useVillageDecorations(housePositions, friendCount) {
  return React.useMemo(() => {
    const items = [];
    // A tiny seeded PRNG (mulberry32). Seed includes friend count so the
    // pattern shifts subtly each time you gain/lose a friend.
    let s = (friendCount * 2654435761) >>> 0;
    const rnd = () => {
      s |= 0; s = (s + 0x6D2B79F5) | 0;
      let t = Math.imul(s ^ (s >>> 15), 1 | s);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
    const tooClose = (x, y) => {
      // Centre + friends + already-placed decos — minimum padding so we
      // don't draw a tree on top of a roof.
      if (Math.hypot(x - 50, y - 50) < 14) return true;
      for (const p of housePositions) {
        if (Math.hypot(x - p.x, y - p.y) < 13) return true;
      }
      for (const d of items) {
        if (Math.hypot(x - d.x, y - d.y) < 7) return true;
      }
      return false;
    };
    // Aim for a stable density: ~14 decorations regardless of count, so
    // small villages still feel populated and big ones don't get cluttered.
    const target = 14;
    let tries = 0;
    while (items.length < target && tries < 180) {
      tries++;
      const x = 6 + rnd() * 88;
      const y = 6 + rnd() * 88;
      if (tooClose(x, y)) continue;
      const r = rnd();
      const kind = r < 0.55 ? 'tree' : r < 0.85 ? 'bush' : 'rock';
      items.push({ x, y, kind, scale: 0.75 + rnd() * 0.5 });
    }
    return items;
  }, [housePositions, friendCount]);
}

// One marker on the village map: a small interactive house anchored at
// the given (x,y) in percent coordinates. We position the wrapper with CSS
// because the inner content is a regular DOM tree (so the friend's name
// is real text, not SVG — better for accessibility + selection).
function VillageMarker({ x, y, friend, href, onRemove, label, isCenter }) {
  const size = isCenter ? 38 : 32;
  const Wrap = href ? 'a' : 'div';
  const wrapProps = href
    ? { href, target: '_blank', rel: 'noopener noreferrer',
        style: { textDecoration: 'none', color: '#1c0828' } }
    : {};
  return (
    <div style={{
      position: 'absolute',
      left: `${x}%`, top: `${y}%`,
      transform: 'translate(-50%, -50%)',
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      gap: 2, zIndex: isCenter ? 4 : 3,
      maxWidth: 76,
    }}>
      <Wrap {...wrapProps}>
        <MiniHouse friend={friend} size={size}/>
      </Wrap>
      <div style={{
        background: isCenter ? '#FFE08A' : '#FFF8E0',
        border: '1.5px solid #111',
        borderRadius: 3,
        padding: '1px 5px',
        fontFamily: '"Fredoka", system-ui',
        fontWeight: 700, fontSize: 9.5,
        color: '#1c0828',
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        maxWidth: 70,
      }}>
        {label || ('@' + friend.pseudo)}
      </div>
      {onRemove && (
        <button onClick={onRemove}
          style={{
            position: 'absolute', top: -7, right: -7,
            appearance: 'none', padding: 0,
            width: 18, height: 18, borderRadius: '50%',
            background: '#E15B5B', border: '1.5px solid #111',
            color: '#fff', cursor: 'pointer',
            fontFamily: '"Fredoka", system-ui', fontWeight: 700, fontSize: 11,
            lineHeight: 1, boxShadow: '1.5px 1.5px 0 #111', zIndex: 5,
          }} aria-label={`retirer @${friend.pseudo}`}>×</button>
      )}
    </div>
  );
}

// Tiny SVG decorations used by the village map. All take percent
// coordinates (cx, cy) and a scale so they line up with the SVG viewbox.
function VillageTree({ cx, cy, scale = 1 }) {
  const r = 2.2 * scale;
  return (
    <g>
      {/* trunk */}
      <rect x={cx - 0.5} y={cy - 0.5} width="1" height={3.4 * scale}
        fill="#5A4A2E" stroke="#111" strokeWidth="0.4"/>
      {/* canopy */}
      <circle cx={cx} cy={cy - r} r={r} fill="#4F8244"
        stroke="#111" strokeWidth="0.45"/>
      {/* tiny red fruit specks */}
      <circle cx={cx - r * 0.4} cy={cy - r * 1.1} r="0.35" fill="#E15B5B"/>
      <circle cx={cx + r * 0.4} cy={cy - r * 0.7} r="0.35" fill="#E15B5B"/>
    </g>
  );
}

function VillageBush({ cx, cy, scale = 1 }) {
  const r = 1.4 * scale;
  return (
    <g>
      <circle cx={cx - r * 0.7} cy={cy} r={r} fill="#5E9E51"
        stroke="#111" strokeWidth="0.4"/>
      <circle cx={cx + r * 0.7} cy={cy} r={r * 0.85} fill="#5E9E51"
        stroke="#111" strokeWidth="0.4"/>
      <circle cx={cx} cy={cy - r * 0.4} r={r * 1.1} fill="#6BAE5C"
        stroke="#111" strokeWidth="0.4"/>
    </g>
  );
}

function VillageRock({ cx, cy, scale = 1 }) {
  const r = 1.2 * scale;
  return (
    <ellipse cx={cx} cy={cy} rx={r * 1.2} ry={r * 0.7}
      fill="#A9A29A" stroke="#111" strokeWidth="0.4"/>
  );
}

// Backwards-compatible alias: house-app.jsx and the visitor branch still
// call <MiniHouseGrid …/>; we just redirect to the new village map. The
// `friends` array shape didn't change. centerLabel lets the visitor view
// show the owner's pseudo at the centre instead of "moi".
function MiniHouseGrid({ friends, onRemove, canRemove, centerLabel }) {
  return (
    <Village
      friends={friends}
      onRemove={onRemove}
      canRemove={canRemove}
      ownerPseudo={centerLabel || 'moi'}
    />
  );
}

function RequestRow({ friend, onAccept, onRemove, kind }) {
  return (
    <div style={{
      display:'flex', alignItems:'center', gap: 10,
      background:'#fff', border:'2px solid #111', borderRadius: 10,
      padding: '8px 10px', boxShadow:'2px 2px 0 #111', marginBottom: 8,
    }}>
      <a href={`/${friend.pseudo}/house`} target="_blank" rel="noopener noreferrer">
        <MiniHouse friend={friend} size={42}/>
      </a>
      <div style={{ flex:1, minWidth:0 }}>
        <div style={{ fontFamily:'"Fredoka", system-ui', fontWeight: 600, fontSize: 13,
          whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>
          @{friend.pseudo}
        </div>
        {friend.displayName && friend.displayName !== friend.pseudo && (
          <div style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 10,
            color:'rgba(0,0,0,0.55)', whiteSpace:'nowrap', overflow:'hidden',
            textOverflow:'ellipsis' }}>
            {friend.displayName}
          </div>
        )}
      </div>
      {kind === 'incoming' && (
        <>
          <button onClick={onAccept} style={smallBtnStyle('#5E9E51')}>{ht('house.nb.accept', 'accepter')}</button>
          <button onClick={onRemove} style={smallBtnStyle('#FBE0E0')}>{ht('house.nb.refuse', 'refuser')}</button>
        </>
      )}
      {kind === 'outgoing' && (
        <button onClick={onRemove} style={smallBtnStyle('#FBE0E0')}>{ht('common.cancel', 'annuler')}</button>
      )}
      {kind === 'discover' && (
        <button onClick={onAccept} style={smallBtnStyle('#FFE08A')}>{ht('house.nb.add_friend', '+ ami')}</button>
      )}
    </div>
  );
}

Object.assign(window, { Sheet, RoomEditor, RoomViewer, AddRoomSheet, MailboxSheet, ProfileSheet, RoofSheet, GardenSheet, WallReplySheet, NeighbourhoodSheet });
