// Harvest & Home — the card-only game (Forage), playable solo vs bots.
// Runs entirely client-side on window.HH.Forage (forage-engine.js) and
// window.HH.FORAGE_CARDS (forage-cards.js).

/* eslint-disable no-undef */

const BIOME_TONE = {
  Forest: "#496f48", "Forest Edge": "#587142", Meadow: "#8b7434",
  Wetland: "#477981", Disturbed: "#8a664e", Mountain: "#697176",
};

// Procedural sound kit (no-op fallback when sound.js / AudioContext absent).
const SOUND = (typeof window !== "undefined" && window.HH && window.HH.Sound) || {
  play() {}, getMuted() { return false; }, setMuted() {}, toggle() { return false; },
};
function SoundButton({ muted, onToggle }) {
  return (
    <button className="hh-btn ghost" onClick={onToggle} aria-label={muted ? "Unmute" : "Mute"}
      title={muted ? "Sound off — click for sound" : "Sound on"} style={{ minWidth: 38 }}>
      {muted ? "🔇" : "🔊"}
    </button>
  );
}

function cardTidbits(card) {
  return (card.foraging_tidbits || "")
    .split(";")
    .map((t) => t.trim())
    .filter(Boolean)
    .map((t) => t.charAt(0).toUpperCase() + t.slice(1));
}

function ForageCardFace({ card, season, selected, onClick, dim, cornerBadge }) {
  const guidance = card.card_type === "guidance";
  const mushroom = card.card_type === "mushroom";
  const tidbits = cardTidbits(card);
  const tone = BIOME_TONE[card.biome] || "var(--sepia)";
  const seasonShort = { Spring: "Spr", Summer: "Sum", Fall: "Fal", Winter: "Win" };
  // Layout mirrors the printed cards (63x88mm, 5:7): a 4-row grid of
  // title / art+info / notes / seasonal yields, kept compact so the whole
  // card stays at the print proportions instead of a tall stack.
  return (
    <article
      className={`hh-card fg-card${selected ? " sel" : ""}${guidance ? " guidance" : ""}`}
      onClick={onClick}
      style={{ cursor: onClick ? "pointer" : "default", opacity: dim ? 0.55 : 1, borderColor: selected ? "var(--accent)" : undefined }}
    >
      <div className="fg-c-title">
        <div className="name">{card.name}</div>
        <div className="sci">{card.scientific_name || (guidance ? "a rule of the forager" : "")}</div>
      </div>

      <div className="fg-c-top">
        <div className="hh-art-frame fg-c-art">
          {card.art_path ? (
            <img src={card.art_path} alt="" loading="lazy" style={{ mixBlendMode: "multiply" }} />
          ) : guidance ? (
            <div className="fg-codex-art"><span>❦</span></div>
          ) : (
            <SmallCaps>plate</SmallCaps>
          )}
          {mushroom && <span className="fg-myco" title="mushroom — wildcard: may double-stack a filled layer (one per biome forest); weevil-proof; unlocks the Mycorrhizal slot">🍄</span>}
          {cornerBadge != null && <span className="fg-pay-badge">{cornerBadge}</span>}
        </div>
        <dl className="fg-c-info">
          {!guidance && (
            <div>
              <dt>{(card.biomes || []).length > 1 ? "Biomes" : "Biome"}</dt>
              <dd style={{ color: tone }}>{(card.biomes || [card.biome]).join(" · ")}</dd>
            </div>
          )}
          <div>
            <dt>{guidance ? "Type" : (card.layers || []).length > 1 ? "Layers" : "Layer"}</dt>
            <dd>{guidance ? "Codex" : (card.layers || []).join(" / ")}</dd>
          </div>
        </dl>
      </div>

      {(mushroom || card.id_feature || tidbits.length > 0) && (
        <div className="fg-c-notes">
          {mushroom && (
            <p><strong>Wildcard</strong> May double-stack a filled layer — one mushroom per biome forest.</p>
          )}
          {card.id_feature && (
            <p><strong>{guidance ? "Rule" : "How to identify"}</strong> {card.id_feature}</p>
          )}
          {tidbits.length > 0 && (
            // Match the printed faces: mushrooms trade a tidbit for the
            // wildcard line; the codex cards carry all three of theirs.
            <ul>{tidbits.slice(0, mushroom ? 1 : guidance ? 3 : 2).map((t, i) => <li key={i}>{t}</li>)}</ul>
          )}
        </div>
      )}

      {!guidance && card.yields && (
        <div className="yields fg-c-yields">
          {window.HH.Forage.SEASONS.map((s) => (
            <div key={s} className={`col${s === season ? " active" : ""}`}>
              <span className="s">{seasonShort[s]}</span>
              <YieldIcons y={card.yields[s] || {}} />
            </div>
          ))}
        </div>
      )}
    </article>
  );
}

function ScoreBar({ value, target, label, tone }) {
  const pct = Math.min(100, Math.round((value / target) * 100));
  return (
    <div style={{ flex: 1, minWidth: 0 }}>
      <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11 }}>
        <SmallCaps className="muted">{label}</SmallCaps>
        <span className="hh-mono" style={{ color: "var(--ink)" }}>{value} / {target}</span>
      </div>
      <div style={{ height: 6, background: "var(--paper-deep)", border: "1px solid var(--rule-soft)", marginTop: 3 }}>
        <div style={{ width: `${pct}%`, height: "100%", background: tone || "var(--sepia)", transition: "width .4s ease" }} />
      </div>
    </div>
  );
}

const SEAT_COLORS = ["#b84d3f", "#4f7a3f", "#477981", "#8b7434", "#7a5a8a", "#8a664e"];
function seatColor(seat) { return SEAT_COLORS[seat % SEAT_COLORS.length]; }

// A status-ribbon chip: a compact, always-visible summary that expands into a
// floating panel on click. The panel is absolutely positioned so opening it
// never pushes the cards below down. `extra` renders an absolutely-positioned
// decoration inside the wrap (e.g. the build score-pop).
function RailChip({ id, open, onToggle, label, dot, tone, align, extra, children }) {
  return (
    <div className={`fg-chip-wrap${open ? " open" : ""}`}>
      {extra}
      <button type="button" className={`fg-chip${tone ? " fg-chip-" + tone : ""}`}
        aria-expanded={open} onClick={() => onToggle(id)}>
        {dot && <span className="fg-opp-dot" style={{ background: dot }} />}
        {label}
        <span className="fg-chip-caret" aria-hidden="true">{open ? "▾" : "▸"}</span>
      </button>
      {open && <div className={`fg-chip-panel${align === "right" ? " right" : ""}`}>{children}</div>}
    </div>
  );
}

function OpponentStrip({ players, activeSeat, onInspect }) {
  const others = players.filter((p) => p.seat !== activeSeat);
  return (
    <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
      {others.map((p) => (
        <button key={p.seat} type="button" className="fg-opp" onClick={() => onInspect(p.seat)}
          title={p.forests.length ? `View ${p.name}'s built forests & cards` : `${p.name} hasn't built anything yet`}>
          <span className="fg-opp-dot" style={{ background: seatColor(p.seat) }} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontWeight: 600, fontSize: 13, display: "flex", alignItems: "center", gap: 4 }}>
              {p.name}{!p.isHuman && <BotIcon size={11} />}
            </div>
            <SmallCaps className="muted">{p.score} pts · {p.biomesBuilt.length} biome{p.biomesBuilt.length === 1 ? "" : "s"} · {p.hand.length} cards</SmallCaps>
          </div>
          <span className="fg-opp-view" aria-hidden="true">{p.forests.length ? `${p.forests.length} ▸` : ""}</span>
        </button>
      ))}
    </div>
  );
}

const BUILD_GLYPH = { biome: "🌲", layer_set: "🌾", mushroom_set: "🍄", guidance_set: "❦" };
// "Meadow forest", "Wetland forest" … but just "Forest" / "Forest Edge" when
// the biome name already carries the word (no "Forest forest").
function biomeForestLabel(biome) {
  return /forest/i.test(biome) ? biome : `${biome} forest`;
}
function buildKindLabel(f) {
  if (f.kind === "biome") return biomeForestLabel(f.biome);
  if (f.kind === "layer_set") return f.layer ? `${f.layer} layer set` : "Layer set";
  if (f.kind === "mushroom_set") return "Mushroom set";
  return "Forager's Codex";
}

// Compact, clickable list of a player's committed builds (in the harvest rail).
function BuiltStrip({ player, onOpen }) {
  if (!player.forests.length) return null;
  return (
    <div className="fg-built">
      {player.forests.map((f, i) => (
        <button key={i} type="button" className="fg-built-chip" onClick={onOpen}
          title={`${buildKindLabel(f)} — ${(f.cards || []).map((c) => c.name).join(", ")}`}>
          <span>{BUILD_GLYPH[f.kind] || "❦"}</span>
          <span className="fg-built-chip-label">{f.kind === "biome" ? f.biome : buildKindLabel(f)}</span>
          <span className="fg-built-chip-score">+{f.score}</span>
        </button>
      ))}
    </div>
  );
}

// Full-screen look at every forest/set a player has committed, with the
// actual cards face-up. Built cards are public information once scored.
function BuildInspector({ player, season, onClose }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  if (!player) return null;
  const forests = player.forests || [];
  const title = player.name === "You" ? "Your builds" : `${player.name}'s builds`;
  return (
    <div className="hh-overlay" onClick={(e) => { if (e.target.classList.contains("hh-overlay")) onClose(); }}>
      <div className="panel">
        <header>
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
            <span className="fg-opp-dot" style={{ background: seatColor(player.seat), width: 14, height: 14 }} />
            <div>
              <SmallCaps>{title}</SmallCaps>
              <h2>{forests.length} build{forests.length === 1 ? "" : "s"} · {player.biomesBuilt.length} biome{player.biomesBuilt.length === 1 ? "" : "s"} · {player.score} pts</h2>
            </div>
          </div>
          <button className="hh-btn icon-btn" onClick={onClose} aria-label="Close"><CloseIcon /></button>
        </header>
        <div className="body">
          {forests.length === 0 ? (
            <Notice>No builds yet — this forager hasn't committed any cards to a forest or set.</Notice>
          ) : (
            forests.map((f, i) => (
              <section key={i} className="fg-build-group">
                <div className="fg-build-group-head">
                  <span className="fg-build-glyph">{BUILD_GLYPH[f.kind] || "❦"}</span>
                  <SmallCaps>{buildKindLabel(f)}</SmallCaps>
                  <span className="fg-build-meta">
                    {f.layerCount} {f.kind === "biome" ? "layers" : "cards"}{f.kind === "layer_set" && f.layer ? ` · ${f.layer}` : ""} · built in {f.season} · <strong>+{f.score}</strong>
                  </span>
                </div>
                <div className="fg-row fg-build-cards">
                  {(f.cards || []).map((c) => <ForageCardFace key={c.uid} card={c} season={season} />)}
                </div>
              </section>
            ))
          )}
        </div>
      </div>
    </div>
  );
}

// Recap shown when another player (usually a bot) commits a set on their turn:
// who played it, the cards face-up, and the points scored. Dismiss by clicking
// the backdrop, the Dismiss button, or Escape.
function BuildRecap({ recap, season, onDismiss }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onDismiss(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onDismiss]);
  if (!recap) return null;
  const { name, seat, forest } = recap;
  return (
    <div className="hh-overlay" onClick={(e) => { if (e.target.classList.contains("hh-overlay")) onDismiss(); }}>
      <div className="panel fg-recap">
        <header>
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
            <span className="fg-opp-dot" style={{ background: seatColor(seat), width: 14, height: 14 }} />
            <div>
              <SmallCaps>{name} played a set</SmallCaps>
              <h2>{BUILD_GLYPH[forest.kind] || "❦"}&nbsp; {buildKindLabel(forest)} <span className="fg-recap-score">+{forest.score} pts</span></h2>
            </div>
          </div>
          <button className="hh-btn icon-btn" onClick={onDismiss} aria-label="Close"><CloseIcon /></button>
        </header>
        <div className="body">
          <div className="fg-row fg-build-cards">
            {(forest.cards || []).map((c) => <ForageCardFace key={c.uid} card={c} season={season} />)}
          </div>
        </div>
        <footer className="fg-recap-foot">
          <button className="hh-btn primary" onClick={onDismiss}>Dismiss</button>
        </footer>
      </div>
    </div>
  );
}

// The draw pile, sitting in the market row as a full card showing the BACK of
// the next card (its botanical plate, no stats) — the same card that, if you
// don't draw it as your free pick, refills the market next. Same size as every
// other card so the row stays uniform.
function DeckCard({ next, count, disabled, onClick, freeUsed }) {
  return (
    <div className="fg-cardwrap">
      <div className="fg-slot-tag">next · face-down</div>
      <article className="hh-card fg-card fg-card-back"
        title="The back of the next card. Draw it as your free pick, or leave it — it refills the market next.">
        {next && next.art_path ? (
          <img className="fg-back-art" src={next.art_path} alt="back of the next card" loading="lazy" />
        ) : (
          <span className="fg-back-motif">❦</span>
        )}
        <span className="fg-back-label">{next ? "next card · face down" : "deck empty"}</span>
      </article>
      <div className="fg-price">{freeUsed ? "pulled this turn" : "your free pick"}</div>
      <button type="button" className="hh-btn primary fg-take" disabled={disabled} onClick={onClick}
        title={freeUsed
          ? "You've already pulled a card this turn — one pull per turn (draw or market)"
          : "Draw the next card from the deck as your one pull. If you don't, it refills the market next."}>
        {freeUsed ? "✓ pulled" : "Draw · free"}
      </button>
    </div>
  );
}

const FG_PRESETS = [
  { key: "sprint", name: "Sprint", blurb: "25 points · 3 biomes · ~10 min" },
  { key: "standard", name: "Standard", blurb: "42 points · 4 biomes · ~20 min" },
  { key: "marathon", name: "Marathon", blurb: "55 points · 5 biomes · ~60 min" },
];
function ForageSetup({ config, onStart }) {
  const [numHumans, setNumHumans] = React.useState(config.numHumans);
  const [numBots, setNumBots] = React.useState(config.numBots);
  const [preset, setPreset] = React.useState(config.preset || "standard");
  const [communityForests, setCommunityForests] = React.useState(!!config.communityForests);
  const total = numHumans + numBots;
  const tooFew = total < 2;
  const tooMany = total > 6;

  // One editable name per human player, kept in sync as the count changes
  // (existing edits are preserved; new slots default to "Player N").
  const [names, setNames] = React.useState(() =>
    Array.from({ length: config.numHumans }, (_, i) => `Player ${i + 1}`));
  React.useEffect(() => {
    setNames((prev) => Array.from({ length: numHumans }, (_, i) =>
      prev[i] != null ? prev[i] : `Player ${i + 1}`));
  }, [numHumans]);
  const setName = (i, v) => setNames((prev) => prev.map((p, j) => (j === i ? v : p)));
  const humanNames = numHumans > 1
    ? names.map((n, i) => (n.trim() || `Player ${i + 1}`))
    : null;

  function Stepper({ label, value, set, min, max, hint }) {
    return (
      <div className="fg-setup-row">
        <div>
          <div style={{ font: "500 16px 'EB Garamond', serif", color: "var(--ink)" }}>{label}</div>
          <SmallCaps className="muted">{hint}</SmallCaps>
        </div>
        <div className="fg-stepper">
          <button className="hh-btn" disabled={value <= min} onClick={() => set(value - 1)} aria-label={`fewer ${label}`}>−</button>
          <span className="fg-stepper-val hh-mono">{value}</span>
          <button className="hh-btn" disabled={value >= max} onClick={() => set(value + 1)} aria-label={`more ${label}`}>+</button>
        </div>
      </div>
    );
  }

  return (
    <div className="fg-setup">
      <div className="fg-setup-panel hh-plate">
        <SmallCaps>New game</SmallCaps>
        <h2 style={{ font: "500 30px/1.1 'EB Garamond', serif", color: "var(--ink)", margin: "4px 0 14px" }}>
          Who's foraging?
        </h2>

        <Stepper label="Human players" value={numHumans} set={setNumHumans} min={1} max={5} hint="pass-and-play on one device" />
        <Stepper label="Bot players" value={numBots} set={setNumBots} min={0} max={5} hint="the wild's other foragers" />

        <div className="fg-setup-row">
          <div>
            <div style={{ font: "500 16px 'EB Garamond', serif", color: "var(--ink)" }}>Game length</div>
            <SmallCaps className="muted">{FG_PRESETS.find((p) => p.key === preset).blurb}</SmallCaps>
          </div>
          <div className="fg-stepper" role="group" aria-label="Game length">
            {FG_PRESETS.map((p) => (
              <button key={p.key} type="button"
                className={`hh-btn${preset === p.key ? " primary" : ""}`}
                aria-pressed={preset === p.key}
                onClick={() => setPreset(p.key)}>
                {p.name}
              </button>
            ))}
          </div>
        </div>

        <div className="fg-setup-row">
          <div>
            <div style={{ font: "500 16px 'EB Garamond', serif", color: "var(--ink)" }}>
              Community Forests <span style={{ font: "600 10px 'IBM Plex Mono', monospace", letterSpacing: ".08em", textTransform: "uppercase", color: "var(--accent)", border: "1px solid var(--accent)", borderRadius: 4, padding: "1px 5px", marginLeft: 4, verticalAlign: "middle" }}>Advanced</span>
            </div>
            <SmallCaps className="muted">
              {communityForests
                ? "Finish rivals' forests for points — learn the base game first"
                : "Off — build your own forests (recommended for new players)"}
            </SmallCaps>
          </div>
          <button type="button"
            className={`hh-btn${communityForests ? " primary" : ""}`}
            role="switch" aria-checked={communityForests}
            aria-label="Toggle Community Forests advanced mode"
            onClick={() => setCommunityForests((v) => !v)}>
            {communityForests ? "On" : "Off"}
          </button>
        </div>

        {numHumans > 1 && (
          <div className="fg-setup-names">
            <SmallCaps className="muted">Name the players</SmallCaps>
            {names.map((nm, i) => (
              <div key={i} className="fg-setup-name-row">
                <span className="fg-opp-dot" style={{ background: seatColor(i) }} />
                <input
                  className="fg-setup-name-input"
                  value={nm}
                  maxLength={24}
                  placeholder={`Player ${i + 1}`}
                  aria-label={`Name for player ${i + 1}`}
                  onChange={(e) => setName(i, e.target.value)}
                />
              </div>
            ))}
          </div>
        )}

        <div style={{ marginTop: 8, font: "italic 400 13px 'EB Garamond', serif", color: tooFew || tooMany ? "var(--accent)" : "var(--ink-soft)" }}>
          {tooFew ? "Add at least one more forager (2 players minimum)."
            : tooMany ? "That's a crowd — keep it to 6 foragers or fewer."
            : numHumans > 1
            ? `${total} foragers · ${numHumans} share this device — hands stay hidden until each player reveals.`
            : `${total} foragers · you vs ${numBots} bot${numBots === 1 ? "" : "s"}.`}
        </div>

        <button className="hh-btn primary xl" style={{ marginTop: 16, width: "100%" }}
          disabled={tooFew || tooMany}
          onClick={() => onStart({ numHumans, numBots, preset, marketMode: "slots", humanNames, communityForests })}>
          ❦ &nbsp; Start foraging &nbsp; ❦
        </button>
      </div>
    </div>
  );
}

// Hot-seat privacy: between human turns, cover the screen until the next
// player confirms they hold the device, so nobody sees the prior hand.
function HandoffOverlay({ name, seat, giftTo, discardEvent, onReveal }) {
  return (
    <div className="fg-handoff">
      <div className="fg-handoff-card">
        <span className="fg-handoff-dot" style={{ background: seatColor(seat) }} />
        <SmallCaps>Pass the device</SmallCaps>
        <h2 style={{ font: "500 30px/1.1 'EB Garamond', serif", color: "var(--ink)", margin: "6px 0 4px" }}>{name}, you're up</h2>
        <p style={{ font: "italic 400 15px/1.5 'EB Garamond', serif", color: "var(--ink-soft)", margin: "0 0 16px", maxWidth: 360 }}>
          {discardEvent
            ? <>{discardEvent} — you'll privately make your choice for this event.</>
            : giftTo
            ? <>A card to pass — you'll privately choose one to give to <strong>{giftTo}</strong>.</>
            : "Make sure the previous forager has looked away, then reveal your private hand."}
        </p>
        <button className="hh-btn primary xl" onClick={onReveal}>Reveal {name}'s hand →</button>
      </div>
    </div>
  );
}

// What the named player must do (or what just happened to them) for the season
// event now on the table — drives the line on the season-change card.
function seasonAckAction(game, seat) {
  const ev = game.event;
  if (!ev || ev.neutralized || ev.name === "Still Day" || ev.name === "Mild Season") {
    return { needsAction: false, text: "A calm turn of the season — no action needed. Forage on." };
  }
  const inQueue = (cur, q, key) => cur && (cur[key] === seat || (q || []).includes(seat));
  const pd = game.pendingDiscard;
  if (pd) {
    const owes = inQueue(pd, game.pendingDiscardQueue, "seat");
    const mode = pd.mode === "in_season" ? "in-season " : pd.mode === "out_of_season" ? "out-of-season " : "";
    return owes
      ? { needsAction: true, text: `You'll choose which ${mode}card(s) to discard once every forager has read the card.` }
      : { needsAction: false, text: "Nothing of yours is at risk — no discard for you." };
  }
  const pc = game.pendingChoice;
  if (pc) {
    const owes = inQueue(pc, game.pendingChoiceQueue, "seat");
    return owes
      ? { needsAction: true, text: "You'll choose how to take the loss once every forager has read the card." }
      : { needsAction: false, text: "Nothing of yours is affected this round." };
  }
  const pb = game.pendingBoon;
  if (pb) {
    const owes = inQueue(pb, game.pendingBoonQueue, "seat");
    return owes
      ? { needsAction: true, text: "You'll choose whether to use this boon once every forager has read the card." }
      : { needsAction: false, text: "You've no card that qualifies — nothing to gain here." };
  }
  const ps = game.pendingSwap;
  if (ps) {
    const owes = inQueue(ps, game.pendingSwapQueue, "seat");
    return owes
      ? { needsAction: true, text: "You'll choose whether to swap a card at the fair once every forager has read the card." }
      : { needsAction: false, text: "No swap available to you this round." };
  }
  const pg = game.pendingGift;
  if (pg) {
    const owes = inQueue(pg, game.pendingGiftQueue, "from");
    const side = pg.dir === "right" ? "right" : "left";
    return owes
      ? { needsAction: true, text: `You'll pass one card to the forager on your ${side} once every forager has read the card.` }
      : { needsAction: false, text: "You've nothing to pass this round." };
  }
  switch (ev.name) {
    case "Bounty":           return { needsAction: false, text: "Two fresh cards have already been added to your hand." };
    case "Open Market":      return { needsAction: false, text: "A free market card has already been added to your hand." };
    case "Knowledge Keeper": return { needsAction: false, text: "The best of the top three deck cards is already in your hand." };
    case "Mentor's Visit":   return { needsAction: false, text: "The best of the top two deck cards is already in your hand." };
    case "Forager's Cache":  return { needsAction: false, text: "A card from the discard pile is already in your hand." };
    case "Overgrowth":       return { needsAction: false, text: "Fresh cards have already been added to your hand." };
    default:                 return { needsAction: false, text: "No action needed — forage on." };
  }
}

// Season turn-over gate: every human reads and dismisses the new season's card
// (one at a time, hot-seat private) and performs the action it calls for.
function SeasonChangeModal({ season, event, playerName, seat, multi, needHandoff, action, gained, lost, onReveal, onAck }) {
  // Gate the action until the flip has revealed the card front. Reset on the
  // render where the event changes (a new season card needs a fresh reveal).
  // Done during render — not in an effect — so it can't race the child flip's
  // onRevealed callback (child effects fire before parent effects, which under
  // reduced motion would otherwise re-gate an already-revealed card).
  const [revealed, setRevealed] = React.useState(false);
  const evName = event ? event.name : null;
  const prevEvName = React.useRef(evName);
  if (prevEvName.current !== evName) {
    prevEvName.current = evName;
    if (revealed) setRevealed(false);
  }
  if (needHandoff) {
    return (
      <div className="fg-handoff">
        <div className="fg-handoff-card">
          <span className="fg-handoff-dot" style={{ background: seatColor(seat) }} />
          <SmallCaps>The season turns</SmallCaps>
          <h2 style={{ font: "500 30px/1.1 'EB Garamond', serif", color: "var(--ink)", margin: "6px 0 4px" }}>{season} arrives</h2>
          <p style={{ font: "italic 400 15px/1.5 'EB Garamond', serif", color: "var(--ink-soft)", margin: "0 0 16px", maxWidth: 360 }}>
            Pass the device to <strong>{playerName}</strong> so they can read this season's card.
          </p>
          <button className="hh-btn primary xl" onClick={onReveal}>Show {playerName} the season card →</button>
        </div>
      </div>
    );
  }
  return (
    <div className="fg-handoff">
      <div className="fg-handoff-card fg-season-modal">
        <SmallCaps>{season} arrives{multi ? ` · ${playerName}` : ""}</SmallCaps>
        <div className="fg-season-modal-card">
          <EventCardFlip
            event={event}
            key={event ? event.name : "none"}
            onRevealed={() => setRevealed(true)}
          />
        </div>
        {/* The action prompt + button stay gated until the card has flipped to
            its front, so the player reads the rule before acting. */}
        {revealed ? (
          <>
            {gained && gained.length > 0 && (
              <div className="fg-season-gained">
                <SmallCaps>{gained.length === 1 ? "Added to your hand" : `Added to your hand · ${gained.length}`}</SmallCaps>
                <div className="fg-season-gained-row">
                  {gained.map((c) => (
                    <span className="fg-gained-card" key={c.uid}>
                      {c.art_path && <img src={c.art_path} alt="" />}
                      <span className="fg-gained-name">{c.name}</span>
                    </span>
                  ))}
                </div>
              </div>
            )}
            {lost && lost.length > 0 && (
              <div className="fg-season-gained fg-season-lost">
                <SmallCaps>{lost.length === 1 ? "Lost from your hand" : `Lost from your hand · ${lost.length}`}</SmallCaps>
                <div className="fg-season-gained-row">
                  {lost.map((c) => (
                    <span className="fg-gained-card" key={c.uid}>
                      {c.art_path && <img src={c.art_path} alt="" />}
                      <span className="fg-gained-name">{c.name}</span>
                    </span>
                  ))}
                </div>
              </div>
            )}
            {/* Suppress the prose line when the gained/lost chips already say it
                (pure gain or auto-loss events with no action to take). */}
            {(action.needsAction || !((gained && gained.length > 0) || (lost && lost.length > 0))) && (
              <p className={`fg-season-modal-action${action.needsAction ? " do" : ""}`}>
                {action.needsAction && <strong>Action needed — </strong>}
                {action.text}
              </p>
            )}
            <button className="hh-btn primary xl" onClick={onAck}>
              {action.needsAction ? "Got it — continue →" : "Dismiss the card →"}
            </button>
          </>
        ) : (
          <p className="fg-season-modal-gate">
            <span className="fg-gate-glyph">↻</span> Tap the card to flip it and reveal this season's event
          </p>
        )}
      </div>
    </div>
  );
}

const RESOURCE_LABEL = { cal: "Calorie", bot: "Botanical", seed: "Seed" };
const EVENT_GLYPH = {
  // 1-9 (three renamed: Still Day, Infestation, Bounty).
  "Still Day": "☼", "Pest Pressure": "🐞", "Infestation": "🐛",
  "Drying Wind": "🍂", "Hard Frost": "❄", "Bounty": "🌾",
  "Open Market": "🧺", "Knowledge Keeper": "📖", "Forager's Gift": "🤝",
  // 10-24 (expanded deck).
  "Accident": "💥", "Harvest Feast": "🍲", "Reseed": "🌱",
  "Sudden Storm": "⛈", "Drought": "🌵", "Rot": "🍂",
  "Thicket": "🌿", "Wildfire": "🔥", "Forager's Cache": "🎁",
  "Overgrowth": "🌳", "Foraged Medicine": "🌼", "Hearty Provisions": "🥖",
  "Mentor's Visit": "🧑‍🏫", "Market Fair": "🎪", "Generous Neighbor": "💞",
  // legacy aliases (in case an old saved label surfaces).
  "Mild Season": "☼", "Severe Pest Infestation": "🐛", "Bumper Crop": "🌾",
};
const EVENT_SLUG = {
  "Still Day": "mild-season", "Pest Pressure": "pest-pressure",
  "Infestation": "severe-infestation", "Drying Wind": "drying-wind",
  "Hard Frost": "hard-frost", "Bounty": "bumper-crop",
  "Open Market": "open-market", "Knowledge Keeper": "knowledge-keeper",
  "Forager's Gift": "foragers-gift",
  "Accident": "accident", "Harvest Feast": "harvest-feast", "Reseed": "reseed",
  "Sudden Storm": "sudden-storm", "Drought": "drought", "Rot": "rot",
  "Thicket": "thicket", "Wildfire": "wildfire", "Forager's Cache": "foragers-cache",
  "Overgrowth": "overgrowth", "Foraged Medicine": "foraged-medicine",
  "Hearty Provisions": "hearty-provisions", "Mentor's Visit": "mentors-visit",
  "Market Fair": "market-fair", "Generous Neighbor": "generous-neighbor",
  // legacy aliases.
  "Mild Season": "mild-season", "Severe Pest Infestation": "severe-infestation",
  "Bumper Crop": "bumper-crop",
};
function EventCard({ event }) {
  const [artOk, setArtOk] = React.useState(true);
  if (!event) return null;
  const slug = EVENT_SLUG[event.name];
  const art = slug ? `/assets/events/forage/${slug}.png` : null;
  return (
    <div className={`fg-event fg-event-${event.tone}`}>
      <div className="fg-event-glyph">
        {art && artOk
          ? <img src={art} alt="" onError={() => setArtOk(false)} style={{ width: 44, height: 44, objectFit: "contain", mixBlendMode: "multiply" }} />
          : (EVENT_GLYPH[event.name] || "❦")}
      </div>
      <div>
        <SmallCaps>{event.season} · season event card</SmallCaps>
        <div className="fg-event-name">{event.name}</div>
        <div className="fg-event-desc">{event.desc}</div>
      </div>
    </div>
  );
}

function prefersReducedMotion() {
  return typeof window !== "undefined" && window.matchMedia
    && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

function eventArtPath(event) {
  const slug = event && EVENT_SLUG[event.name];
  return slug ? `/assets/events/forage/${slug}.png` : null;
}

// The full event card — a real card at the game's 5:7 proportions, tone-tinted
// (good/bad/calm/social), with the herbarium engraved frame, the event art
// (mix-blend multiply, like the plant plates), the event NAME as title, a
// "<season> · event" kicker, and the rule text as the body.
function EventCardFront({ event }) {
  const [artOk, setArtOk] = React.useState(true);
  if (!event) return null;
  const art = eventArtPath(event);
  return (
    <article className={`hh-card fg-ecard fg-ecard-${event.tone}`}>
      <div className="fg-ecard-title">
        <SmallCaps className="fg-ecard-kicker">{event.season} · event</SmallCaps>
        <div className="fg-ecard-name">{event.name}</div>
      </div>
      <div className="fg-ecard-art">
        {art && artOk
          ? <img src={art} alt="" onError={() => setArtOk(false)} />
          : <span className="fg-ecard-glyph">{EVENT_GLYPH[event.name] || "❦"}</span>}
      </div>
      <div className="fg-ecard-body">
        <p className="fg-ecard-desc">{event.desc}</p>
      </div>
    </article>
  );
}

// The face-down card: herbarium frame + centered art only — no text at all.
function EventCardBack({ event }) {
  const [artOk, setArtOk] = React.useState(true);
  const art = eventArtPath(event);
  return (
    <article className="hh-card fg-ecard fg-ecard-back" aria-hidden="true">
      {art && artOk
        ? <img className="fg-back-art" src={art} alt="" onError={() => setArtOk(false)} />
        : <span className="fg-back-motif">❦</span>}
    </article>
  );
}

// 3-D flip: starts showing the BACK, then flips to reveal the FRONT — after a
// short beat, or immediately when the player taps the card. Calls onRevealed
// once the front is showing so the modal can ungate the action. Honors
// prefers-reduced-motion (shows the front at once, no animation).
function EventCardFlip({ event, onRevealed }) {
  const reduce = prefersReducedMotion();
  const [revealed, setRevealed] = React.useState(reduce);
  const firedRef = React.useRef(false);

  const reveal = React.useCallback(() => {
    setRevealed(true);
  }, []);

  // Tap-driven reveal, with an auto-flip safety net so the action is ALWAYS
  // reachable even if the player never taps (a missed tap on mobile must not
  // strand them before the "Got it" button). Tapping flips instantly. Reduced
  // motion starts already revealed.
  React.useEffect(() => {
    if (reduce || revealed) return undefined;
    const t = setTimeout(() => setRevealed(true), 1600);
    return () => clearTimeout(t);
  }, [reduce, revealed]);

  // Notify the parent once, when the front first becomes visible.
  React.useEffect(() => {
    if (revealed && !firedRef.current) {
      firedRef.current = true;
      if (onRevealed) onRevealed();
    }
  }, [revealed, onRevealed]);

  return (
    <div
      className={`fg-flip${revealed ? " is-revealed" : ""}${reduce ? " no-anim" : ""}`}
      onClick={revealed ? undefined : reveal}
      role="button"
      tabIndex={revealed ? -1 : 0}
      aria-label={revealed ? undefined : "Flip the season event card to reveal it"}
      title={revealed ? undefined : "Tap to flip"}
      onKeyDown={(e) => {
        if (!revealed && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); reveal(); }
      }}
    >
      <div className="fg-flip-inner">
        <div className="fg-flip-face fg-flip-back">
          <EventCardBack event={event} />
        </div>
        <div className="fg-flip-face fg-flip-front">
          <EventCardFront event={event} />
        </div>
      </div>
    </div>
  );
}

const FG_BOT_NAMES = ["Bramble", "Sorrel", "Juniper", "Hazel", "Rowan", "Linden"];
function buildRoster(numHumans, numBots, humanNames) {
  const humans = numHumans === 1
    ? ["You"]
    : Array.from({ length: numHumans }, (_, i) =>
        (humanNames && humanNames[i] && String(humanNames[i]).trim()) || `Player ${i + 1}`);
  return humans.concat(FG_BOT_NAMES.slice(0, numBots));
}

function ForageScreen({ onExit, onOpenRulebook }) {
  const Forage = window.HH.Forage;
  const [phase, setPhase] = React.useState("setup"); // setup | play
  const [config, setConfig] = React.useState({ numHumans: 1, numBots: 3, preset: "standard", marketMode: "slots" });
  const [game, setGame] = React.useState(null);
  const [, force] = React.useReducer((x) => x + 1, 0);
  const [selUid, setSelUid] = React.useState(null);
  const [purchasePopup, setPurchasePopup] = React.useState(false); // explains a market bundle buy
  const [buildDraft, setBuildDraft] = React.useState(null); // { biome, selected: [uid] }
  const [inspectSeat, setInspectSeat] = React.useState(null); // seat whose builds are open
  const [revealedSeat, setRevealedSeat] = React.useState(null);
  const [helpOpen, setHelpOpen] = React.useState(true);
  const [openPanel, setOpenPanel] = React.useState(null); // which status chip is expanded
  const statusRef = React.useRef(null);
  const [recapQueue, setRecapQueue] = React.useState([]); // other players' sets to recap
  const seenBuildsRef = React.useRef({}); // seat -> forests already accounted for
  const [muted, setMuted] = React.useState(SOUND.getMuted());
  const [buildFlash, setBuildFlash] = React.useState(null);   // {id, score}
  const [ackRevealedSeat, setAckRevealedSeat] = React.useState(null); // hot-seat: who's holding the device for the season card
  const logRef = React.useRef(null);
  const seasonRef = React.useRef(0);
  const overRef = React.useRef(false);

  function toggleMute() { const m = SOUND.toggle(); setMuted(m); if (!m) SOUND.play("tick"); }

  function startGame(cfg) {
    const total = cfg.numHumans + cfg.numBots;
    const g = Forage.newGame({
      numPlayers: total, numHumans: cfg.numHumans,
      preset: cfg.preset || "standard",
      names: buildRoster(cfg.numHumans, cfg.numBots, cfg.humanNames),
      config: { marketMode: cfg.marketMode || "slots", allowForestInfill: !!cfg.communityForests },
    });
    seasonRef.current = g.seasonIndex; overRef.current = false;
    seenBuildsRef.current = {}; setRecapQueue([]);
    setConfig(cfg); setGame(g); setSelUid(null); setRevealedSeat(null); setInspectSeat(null);
    setBuildFlash(null); setAckRevealedSeat(null); setPhase("play");
    SOUND.play("take");
  }
  function newGame() { startGame(config); }

  // Active player + hot-seat view state (all safe before a game exists). A
  // pending weevil discard or Forager's Gift hands the spotlight to the owing
  // player before the current turn resumes.
  const humansCount = config.numHumans;
  const activeSeat = (game && !game.over)
    ? (game.pendingSeasonAck ? game.pendingSeasonAck.seat
      : game.pendingDiscard ? game.pendingDiscard.seat
      : game.pendingChoice ? game.pendingChoice.seat
      : game.pendingBoon ? game.pendingBoon.seat
      : game.pendingSwap ? game.pendingSwap.seat
      : game.pendingGift ? game.pendingGift.from
      : game.current)
    : -1;
  const activePlayer = game ? (game.players[activeSeat] || game.players[game.current] || game.players[0]) : null;
  const isHumanActive = !!(game && !game.over && activePlayer && activePlayer.isHuman);
  const needHandoff = isHumanActive && humansCount > 1 && revealedSeat !== activeSeat;
  const seasonAck = (game && !game.over) ? game.pendingSeasonAck : null;
  const eventBusy = !!(game && (game.pendingGift || game.pendingDiscard || game.pendingChoice
    || game.pendingBoon || game.pendingSwap));
  const myTurn = isHumanActive && !eventBusy && !seasonAck && !needHandoff;
  // Whose private hand may show now: the active human, or — solo vs bots —
  // always you, so you can plan while the bots take their turns.
  const viewerSeat = isHumanActive ? activeSeat : (game && humansCount === 1 ? 0 : -1);
  const viewer = (game && viewerSeat >= 0) ? game.players[viewerSeat] : null;
  const showHand = !!viewer && !needHandoff;

  const slotMarket = !!(game && game.cfg.marketMode === "slots");
  const purchase = game && game.pendingPurchase;
  const freeUsed = game ? Forage.freePickUsed(game) : false;
  const freeDrawOk = !!(game && myTurn && !purchase && !buildDraft && Forage.canDrawFree(game));
  const payTotal = purchase ? Forage.purchaseSelectedTotal(game) : 0;
  const purchaseCount = purchase ? (purchase.slotIndex != null ? purchase.slotIndex + 1 : 1) : 0;

  // Pending event interactions owed by the viewing human (if any).
  const discard = game && game.pendingDiscard;
  const choice = game && game.pendingChoice;
  const boon = game && game.pendingBoon;
  const swap = game && game.pendingSwap;
  const boonEligible = boon ? new Set(Forage.boonEligibleUids(game)) : null;
  const swapActive = !!(swap && showHand && swap.seat === viewerSeat);
  const swapHandUid = swapActive && selUid && viewer && viewer.hand.some((c) => c.uid === selUid) ? selUid : null;
  const discardEligible = discard ? new Set(Forage.discardEligibleUids(game)) : null;
  const discardModeLabel = discard
    ? (discard.mode === "in_season" ? "in-season" : discard.mode === "out_of_season" ? "out-of-season" : "")
    : "";

  // Live preview of the biome build the human is composing.
  const buildPreview = (buildDraft && viewer)
    ? (buildDraft.kind === "layer_set"
        ? Forage.evalLayerSetFromSelection(viewer.hand, buildDraft.selected, game.season, game.cfg)
        : Forage.evalBuildFromSelection(viewer.hand, buildDraft.biome, buildDraft.selected, game.season, game.cfg))
    : null;
  // The layer a layer set is locked to, derived from the plants already
  // chosen (so further picks are filtered to that layer even before it's valid).
  const buildSetLayer = (() => {
    if (!buildDraft || buildDraft.kind !== "layer_set" || !viewer) return null;
    const ls = viewer.hand
      .filter((c) => buildDraft.selected.includes(c.uid) && !isImmuneCard(c))
      .map((c) => Forage.effectiveLayer(c))
      .filter(Boolean);
    return ls.length && ls.every((L) => L === ls[0]) ? ls[0] : null;
  })();
  const buildLayerNames = buildPreview
    ? [...buildPreview.layers].filter((L) => !/^Wild /.test(L))
    : [];
  const buildWildCount = buildPreview ? [...buildPreview.layers].filter((L) => /^Wild /.test(L)).length : 0;

  const builds = (myTurn && viewer) ? Forage.availableBuilds(viewer, game.season, game.cfg) : [];
  // Community Forests (advanced): completions you could make on opponents'
  // incomplete forests this turn, best first. The engine auto-picks the
  // cheapest cards that fill the open layers.
  const infills = (myTurn && viewer && game.cfg.allowForestInfill)
    ? Forage.availableInfills(game, viewer) : [];
  const selCard = viewer ? (viewer.hand.find((c) => c.uid === selUid) || null) : null;

  // What biomes (you could still build) does the selected card help with?
  function cardContribution(card) {
    if (!card) return null;
    if (card.card_type === "guidance") return "Wild: fills one missing layer in a forest, or part of the 4-card Codex.";
    if (card.card_type === "mushroom") return `Mushroom: weevil-proof, unlocks the Mycorrhizal slot, and can join a mushroom set. Layers: ${(card.layers || []).join(" / ")}.`;
    const homes = (card.biomes || [card.biome]).join(", ");
    return `Grows in ${homes}. Fills the ${(card.layers || []).join(" / ")} layer.`;
  }

  // Distinct buildable layers the viewer's hand has toward each biome — a
  // "building toward" hint so they can see what's almost ready.
  function biomeProgress(biome) {
    const cards = viewer.hand.filter((c) => Forage.growsIn(c, biome, game.cfg.multiBiome));
    if (!cards.length) return { biome, layers: 0 };
    const hasMush = cards.some(Forage.isMushroom);
    const restricted = new Set(game.cfg.restrictedLayers[biome] || []);
    const opts = cards
      .map((c) => Forage.cardLayers(c, game.cfg.multiLayer).filter((L) => L && (!restricted.has(L) || hasMush)))
      .filter((a) => a.length);
    const cov = Forage.coveredLayers(opts);
    if (hasMush) cov.add("Mycorrhizal");
    return { biome, layers: cov.size };
  }
  const min = game ? game.cfg.qualifyingMinLayers : 3;
  const closest = (myTurn && viewer)
    ? game.cfg.biomes.map(biomeProgress)
        .filter((p) => p.layers > 0 && p.layers < min && !viewer.biomesBuilt.includes(p.biome))
        .sort((a, b) => b.layers - a.layers)[0]
    : null;

  function act(action) {
    const actor = game.players[game.current];
    const before = actor.score;
    Forage.applyAction(game, action);
    const t = action.type || "";
    if (t.startsWith("build")) {
      const gained = actor.score - before;
      if (gained > 0) { setBuildFlash({ id: Date.now(), score: gained }); SOUND.play("build"); }
      else SOUND.play("tick");
    } else if (t === "draw") SOUND.play("draw");
    else if (t === "burn") SOUND.play("discard");
    else SOUND.play("tick");
    setSelUid(null); force();
  }
  // Market takes are a separate phase — they don't end the turn.
  function takeMarketCard(i) {
    const done = Forage.takeMarket(game, i);
    SOUND.play(done ? "take" : "tick");
    setSelUid(null);
    // A paid bundle opens a pending purchase — pop up the cost explanation.
    if (game.pendingPurchase) setPurchasePopup(true);
    force();
  }
  // The deck side of your one free pick — draw 1 blind (does not end the turn).
  function drawFree() { if (Forage.drawFromDeck(game)) { SOUND.play("draw"); setSelUid(null); force(); } }
  function togglePay(uid) { Forage.togglePurchasePay(game, uid); SOUND.play("tick"); force(); }
  function confirmPay() { Forage.confirmPurchase(game); SOUND.play("buy"); setPurchasePopup(false); force(); }
  function cancelPay() { Forage.cancelPurchase(game); SOUND.play("tick"); setPurchasePopup(false); force(); }
  function giftCard(uid) { Forage.resolveGift(game, uid); SOUND.play("gift"); setSelUid(null); force(); }
  function toggleDiscard(uid) { Forage.toggleDiscardPick(game, uid); SOUND.play("tick"); force(); }
  function confirmDiscard() { if (Forage.confirmDiscard(game)) { SOUND.play("discard"); setSelUid(null); force(); } }
  // Expanded-deck interactions.
  function chooseOption(option) { if (Forage.chooseEventOption(game, option)) { SOUND.play("tick"); setSelUid(null); force(); } }
  function resolveBoon(use, uid) { if (Forage.resolveBoon(game, use, uid)) { SOUND.play(use ? "draw" : "tick"); setSelUid(null); force(); } }
  function resolveSwap(opts) { if (Forage.resolveSwap(game, opts)) { SOUND.play(opts && !opts.skip ? "take" : "tick"); setSelUid(null); force(); } }
  // Dismiss the season card and hand off to the next human owed a look (if any).
  function acknowledgeSeasonCard() { Forage.acknowledgeSeason(game); SOUND.play("tick"); setSelUid(null); force(); }

  // Biome-build composer: open with the auto-matched cards pre-selected so a
  // confirm reproduces the old one-click build; the human can then toggle
  // cards (drop the mushroom, swap a layer candidate, retarget a biome).
  function openBuild(b) {
    // Biome forests and layer sets open the card composer; mushroom sets and
    // the Codex are unambiguous, so they still build in a tap.
    if (b.kind !== "biome" && b.kind !== "layer_set") { act(buildActionFor(b)); return; }
    setSelUid(null);
    setBuildDraft({ kind: b.kind, biome: b.biome, selected: b.cards.map((c) => c.uid) });
    SOUND.play("tick");
  }
  function toggleBuildCard(uid) {
    setBuildDraft((d) => {
      if (!d) return d;
      const has = d.selected.includes(uid);
      return { ...d, selected: has ? d.selected.filter((u) => u !== uid) : d.selected.concat(uid) };
    });
    SOUND.play("tick");
  }
  function confirmBuild() {
    if (!buildDraft || !buildPreview || !buildPreview.valid) return;
    if (buildDraft.kind === "layer_set") act({ type: "build_layer_set", cardUids: buildDraft.selected });
    else act({ type: "build_biome", biome: buildDraft.biome, cardUids: buildDraft.selected });
    setBuildDraft(null);
  }
  function cancelBuild() { setBuildDraft(null); SOUND.play("tick"); }

  // Bots play themselves with a readable cadence — paused while a human owes
  // a Forager's Gift choice, or while a set recap is on screen.
  React.useEffect(() => {
    if (!game || game.over || game.pendingSeasonAck || Forage.eventBusy(game)) return;
    if (recapQueue.length) return;
    if (game.players[game.current].isHuman) return;
    const t = setTimeout(() => { Forage.botTakeTurn(game); force(); }, 650);
    return () => clearTimeout(t);
  }, [game, game && game.current, game && game.over, game && game.turnInRound, game && game.seasonIndex,
      game && game.pendingGift, game && game.pendingDiscard, game && game.pendingChoice,
      game && game.pendingBoon, game && game.pendingSwap, game && game.pendingSeasonAck, recapQueue.length]);

  // Watch for sets committed by anyone other than the current viewer (i.e. the
  // bots, or other humans) and queue a recap so the player sees what was built.
  React.useEffect(() => {
    if (!game || game.over) return;
    const seen = seenBuildsRef.current;
    const fresh = [];
    game.players.forEach((p) => {
      const prev = seen[p.seat] || 0;
      if (p.forests.length > prev) {
        for (let i = prev; i < p.forests.length; i++) {
          if (p.seat !== viewerSeat) fresh.push({ name: p.name, seat: p.seat, forest: p.forests[i] });
        }
        seen[p.seat] = p.forests.length;
      }
    });
    if (fresh.length) setRecapQueue((q) => q.concat(fresh));
  });
  const dismissRecap = () => setRecapQueue((q) => q.slice(1));

  // New active human → drop the reveal so the next player must re-confirm, and
  // abandon any half-composed build (it belonged to the previous turn).
  React.useEffect(() => {
    setRevealedSeat((prev) => (prev === activeSeat ? prev : null));
    setBuildDraft(null);
  }, [activeSeat]);

  // Status ribbon: one panel open at a time. (Selecting a hand card no longer
  // pops a detail panel — the card face shows the key info, and selection just
  // highlights it and arms the "Clear" action.)
  const togglePanel = (id) => setOpenPanel((cur) => (cur === id ? null : id));
  React.useEffect(() => {
    if (!openPanel) return;
    const onDown = (e) => {
      if (statusRef.current && !statusRef.current.contains(e.target)) setOpenPanel(null);
    };
    document.addEventListener("mousedown", onDown);
    return () => document.removeEventListener("mousedown", onDown);
  }, [openPanel]);

  // Season turn-over → a cue tinted by the event's tone, and a fresh hand-off
  // for the season card so the first human is re-prompted to take the device.
  React.useEffect(() => {
    if (!game) return;
    if (game.seasonIndex !== seasonRef.current) {
      seasonRef.current = game.seasonIndex;
      setAckRevealedSeat(null);
      const tone = game.event && game.event.tone;
      SOUND.play(tone === "bad" ? "eventBad" : (tone === "good" || tone === "social") ? "eventGood" : "season");
    }
  }, [game && game.seasonIndex]);

  // Game over → fanfare (once).
  React.useEffect(() => {
    if (game && game.over && !overRef.current) { overRef.current = true; SOUND.play("gameover"); }
    if (game && !game.over) overRef.current = false;
  }, [game && game.over]);

  React.useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; });

  // Pre-game setup screen.
  if (phase === "setup" || !game) {
    return (
      <div className="fg-screen">
        <header className="hh-topbar">
          <div className="brand">
            <h1>Forage <span style={{ color: "var(--sepia)" }}>&amp;</span> Forests</h1>
            <span className="sub">a foraging &amp; food-forest card game</span>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
            <SoundButton muted={muted} onToggle={toggleMute} />
            <button className="hh-btn ghost" onClick={onOpenRulebook}><BookIcon size={14} />&nbsp;&nbsp;Rules</button>
            <button className="hh-btn ghost" onClick={onExit}>Exit</button>
          </div>
        </header>
        <ForageSetup config={config} onStart={startGame} />
      </div>
    );
  }

  return (
    <div className="fg-screen">
      <header className="hh-topbar">
        <div className="brand">
          <h1>Forage <span style={{ color: "var(--sepia)" }}>&amp;</span> Forests</h1>
          <span className="sub">a foraging &amp; food-forest card game</span>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
          <span className={`fg-turn${myTurn ? " mine" : ""}`}>
            <span className="fg-turn-dot" style={{ background: seatColor(game.current) }} />
            {game.over ? "Game over"
              : myTurn ? (humansCount > 1 ? `${activePlayer.name}'s turn` : "Your turn")
              : `${game.players[game.current].name} foraging…`}
          </span>
          <SmallCaps className="muted">{game.season} · rd {game.pip}/{game.cfg.roundsPerSeason} · deck {game.deck.length}</SmallCaps>
          <SoundButton muted={muted} onToggle={toggleMute} />
          <button className="hh-btn ghost" onClick={onOpenRulebook}><BookIcon size={14} />&nbsp;&nbsp;Rules</button>
          <button className="hh-btn ghost" onClick={() => setPhase("setup")}>Setup</button>
          <button className="hh-btn ghost" onClick={newGame}>↺ New</button>
          <button className="hh-btn ghost" onClick={onExit}>Exit</button>
        </div>
      </header>
      {seasonAck ? (
        <SeasonChangeModal
          season={game.season}
          event={game.event}
          playerName={activePlayer.name}
          seat={activeSeat}
          multi={humansCount > 1}
          needHandoff={humansCount > 1 && ackRevealedSeat !== seasonAck.seat}
          action={seasonAckAction(game, seasonAck.seat)}
          gained={(game.players[seasonAck.seat] && game.players[seasonAck.seat].eventGain) || []}
          lost={(game.players[seasonAck.seat] && game.players[seasonAck.seat].eventLost) || []}
          onReveal={() => { setAckRevealedSeat(seasonAck.seat); SOUND.play("tick"); }}
          onAck={acknowledgeSeasonCard}
        />
      ) : needHandoff ? (
        <HandoffOverlay
          name={activePlayer.name}
          seat={activeSeat}
          giftTo={game.pendingGift ? game.pendingGift.to : null}
          discardEvent={(game.pendingDiscard && game.pendingDiscard.event)
            || (game.pendingChoice && game.pendingChoice.event)
            || (game.pendingBoon && game.pendingBoon.event)
            || (game.pendingSwap && game.pendingSwap.event) || null}
          onReveal={() => setRevealedSeat(activeSeat)}
        />
      ) : null}

      <div className="fg-body">
        {/* Status ribbon: compact summaries that expand into floating panels
            on demand, so the cards below keep the full screen. */}
        <div className="fg-statusbar" ref={statusRef}>
          {game.event && (
            <RailChip id="event" open={openPanel === "event"} onToggle={togglePanel}
              tone={game.event.tone}
              label={<><span className="fg-chip-glyph">{EVENT_GLYPH[game.event.name] || "❦"}</span>{game.event.name}</>}>
              <EventCard event={game.event} key={game.event.name} />
            </RailChip>
          )}

          <RailChip id="harvest" open={openPanel === "harvest"} onToggle={togglePanel}
            dot={seatColor(activeSeat)}
            extra={buildFlash && (
              <span className="fg-score-pop" key={buildFlash.id} onAnimationEnd={() => setBuildFlash(null)}>
                +{buildFlash.score}
              </span>
            )}
            label={<>
              {humansCount > 1 ? activePlayer.name : "You"}
              <span className="fg-chip-stat">{activePlayer.score}<i>/{game.preset.pointsTarget}</i> pts</span>
              <span className="fg-chip-stat">{activePlayer.biomesBuilt.length}<i>/{game.preset.biomeTarget}</i> 🌲</span>
            </>}>
            <div className="fg-panel-pad">
              <div style={{ display: "flex", gap: 14, margin: "2px 0 6px" }}>
                <ScoreBar value={activePlayer.score} target={game.preset.pointsTarget} label="Points" tone="var(--sepia)" />
                <ScoreBar value={activePlayer.biomesBuilt.length} target={game.preset.biomeTarget} label="Biomes" tone="var(--accent)" />
              </div>
              <SmallCaps className="muted">
                {game.preset.biomeTarget} biomes wins outright · or lead on points at {game.preset.pointsTarget}.
              </SmallCaps>
              {activePlayer.forests.length > 0 && (
                <div style={{ marginTop: 8 }}>
                  <SmallCaps className="muted">Built · tap to see the cards</SmallCaps>
                  <BuiltStrip player={activePlayer} onOpen={() => setInspectSeat(activeSeat)} />
                </div>
              )}
              {closest && (
                <div className="fg-hint">
                  Building toward <strong>{closest.biome}</strong> — {closest.layers}/{min} layers.
                  One more layer there and you can build it.
                </div>
              )}
            </div>
          </RailChip>

          <RailChip id="foragers" open={openPanel === "foragers"} onToggle={togglePanel}
            label={<>
              <span className="fg-chip-rivals-label">Rivals</span>
              {game.players.filter((p) => p.seat !== activeSeat).map((p) => (
                <span key={p.seat} className="fg-chip-rival">
                  <span className="fg-opp-dot" style={{ background: seatColor(p.seat) }} />{p.score}
                </span>
              ))}
            </>}>
            <div className="fg-panel-pad" style={{ minWidth: 280 }}>
              <SmallCaps>The other foragers · tap to see their builds</SmallCaps>
              <div style={{ marginTop: 8 }}><OpponentStrip players={game.players} activeSeat={activeSeat} onInspect={setInspectSeat} /></div>
            </div>
          </RailChip>

          <RailChip id="journal" open={openPanel === "journal"} onToggle={togglePanel}
            label={<>Journal <span className="fg-chip-stat"><i>{game.log.length}</i></span></>}>
            <div className="fg-panel-pad">
              <SmallCaps>Field journal</SmallCaps>
              <div className="fg-log fg-log-panel" ref={logRef}>
                {game.log.map((e, i) => (
                  <div key={i} className="fg-log-line">
                    <span className="hh-mono fg-log-t">{e.t}</span> {e.m}
                  </div>
                ))}
              </div>
            </div>
          </RailChip>

          {/* Tapped-card detail panel removed — the card face shows the key
              info; selecting a card just highlights it and arms "Clear". */}
        </div>

        {/* Main column */}
        <main className="fg-main">
          {game.over ? (
            <GameOver game={game} onNew={newGame} onInspect={setInspectSeat} />
          ) : (
            <>
              {helpOpen && (
                <div className="fg-help">
                  <button className="fg-help-x" onClick={() => setHelpOpen(false)} aria-label="dismiss">✕</button>
                  <SmallCaps>How to forage</SmallCaps>
                  <p>
                    Each turn you get <strong>one card pull</strong>: either
                    <strong> draw 1 card</strong> blind from the deck, or take
                    {slotMarket ? <> the <strong>oldest market card free</strong></> : <> a <strong>free market card</strong></>},
                    {" "}or <strong>buy</strong> one by <strong>trading in</strong> in-season hand cards{slotMarket
                      ? <> — buying a card also sweeps every cheaper card to its left</>
                      : <> ({Forage.MARKET_COST.slice(1).join(", ")} in value)</>}
                    {" "}(a card is worth its current-season Cal+Bot+Seeds total).
                    That's your <strong>one pull</strong> — then take <strong>one main action</strong>:
                    <strong> build</strong>, clear a card, or pass.
                    Collect cards that share a <strong>biome</strong>, then build a
                    <strong> biome forest</strong> (3+ different layers) — every build scores and
                    counts toward the {game.preset.biomeTarget}-biome goal. Also possible:
                    a <strong>layer set</strong> (3+ cards that share one layer, any biome), a
                    <strong> mushroom set</strong>, or the 4-card <strong>Codex</strong>.
                    Mushrooms &amp; guidance cards are weevil-proof and can't be cleared or traded.
                    Build {game.preset.biomeTarget} different biomes to win outright, or be ahead on
                    points when someone hits {game.preset.pointsTarget}. Tap a hand card for its field note.
                  </p>
                  {game.cfg.allowForestInfill && (
                    <p>
                      <strong>Community Forests (advanced) is on.</strong> Instead of building, you may
                      <strong> grow a rival's unfinished forest</strong> — completing missing layers from
                      your hand to score the <strong>points gain</strong> (but no biome credit for you).
                      Watch for <em>⤳ Grow…</em> actions. The flip side: leaving your own forests thin
                      invites rivals to harvest the difference.
                    </p>
                  )}
                </div>
              )}
              <section className="fg-section">
                <div className="fg-market-layout">
                  {/* Draw pile — its own titled group, weighted like the market. */}
                  <div className="fg-market-group fg-market-deck">
                    <div className="fg-section-head">
                      <SmallCaps>Draw pile</SmallCaps>
                      <SmallCaps className="muted">{game.deck.length} left</SmallCaps>
                    </div>
                    <div className="fg-row">
                      <DeckCard
                        next={game.deck[game.deck.length - 1] || null}
                        count={game.deck.length}
                        disabled={!freeDrawOk}
                        onClick={drawFree}
                        freeUsed={freeUsed}
                      />
                    </div>
                  </div>

                  <div className="fg-market-divider" aria-hidden="true" />

                  {/* Marketplace — the face-up row you buy from. */}
                  <div className="fg-market-group fg-market-shop">
                    <div className="fg-section-head">
                      <SmallCaps>Marketplace</SmallCaps>
                      <SmallCaps className="muted">
                        {freeUsed
                          ? "card taken this turn — market closed until your next turn"
                          : slotMarket
                          ? "one pull a turn: take the oldest free, buy a bundle, or draw the next card"
                          : "one pull a turn: take any card free, buy a card, or draw the next card"}
                      </SmallCaps>
                    </div>
                    {slotMarket && (
                      <div className="fg-market-rule">
                        <span className="fg-market-rule-end">⟵ oldest · cheapest</span>
                        <span className="fg-market-rule-mid">
                          buying a card also takes <strong>every card to its left</strong>
                        </span>
                        <span className="fg-market-rule-end">newest · priciest ⟶</span>
                      </div>
                    )}
                    <div className="fg-row">
                      {game.market.map((c, i) => {
                        const cost = Forage.marketBuyCost(game, i);
                        const count = Forage.marketBuyCount(game, i);
                        // One pull per turn: once any card is taken or drawn, the
                        // whole market is closed for the rest of the turn.
                        const pullUsed = freeUsed;
                        // During a slot purchase, the picked card and every cheaper
                        // one to its left form the bundle you'd get — highlight them,
                        // dim the rest.
                        const inBundle = !!(purchase && purchase.slotIndex != null && i <= purchase.slotIndex);
                        const slotLabel = slotMarket
                          ? (i === 0 ? `#1 · oldest` : i === game.market.length - 1 ? `#${i + 1} · newest` : `#${i + 1}`)
                          : null;
                        return (
                          <div key={c.uid} className={`fg-cardwrap${inBundle ? " fg-bundle" : ""}`}>
                            {slotLabel && <div className="fg-slot-tag">{slotLabel}</div>}
                            <ForageCardFace card={c} season={game.season}
                              selected={swapActive ? false : inBundle}
                              dim={swapActive ? !swapHandUid : (buildDraft ? true : (purchase ? !inBundle : pullUsed))} />
                            {swapActive ? (
                              <button
                                className="hh-btn primary fg-take"
                                disabled={!swapHandUid}
                                title={swapHandUid ? "Swap your selected hand card for this market card" : "Pick a hand card below first"}
                                onClick={() => resolveSwap({ handUid: swapHandUid, marketIndex: i })}
                              >Swap here</button>
                            ) : (
                            <button
                              className="hh-btn primary fg-take"
                              disabled={!myTurn || !!purchase || !!buildDraft || pullUsed}
                              title={pullUsed
                                ? "You've already pulled a card this turn — one pull per turn (market or draw)"
                                : slotMarket
                                ? (cost > 0 ? `Take this card and the ${i} card${i === 1 ? "" : "s"} to its left — pay ${cost} by trading in` : "Take the oldest card free — your one pull this turn")
                                : (cost > 0 ? `Trade in in-season cards worth ${cost} to buy this card` : "Your free market card this turn")}
                              onClick={() => takeMarketCard(i)}
                            >
                              {pullUsed ? "✓ pulled"
                                : cost > 0
                                ? (slotMarket ? `Buy cards 1–${count}, Pay ${cost}` : `Buy card, Pay ${cost}`)
                                : "Take · free"}
                            </button>
                            )}
                          </div>
                        );
                      })}
                    </div>
                  </div>
                </div>
              </section>

              <section className="fg-section fg-hand-section">
                <div className="fg-section-head">
                  <SmallCaps>{humansCount > 1 && viewer ? `${viewer.name}'s hand` : "Your hand"} · {viewer ? viewer.hand.length : 0} cards</SmallCaps>
                  <SmallCaps className={eventBusy || purchase || buildDraft ? "accent" : "muted"}>
                    {discard
                      ? `${discard.event} — choose ${discard.required} ${discardModeLabel} card${discard.required === 1 ? "" : "s"} to discard`
                      : choice
                      ? `${choice.event} — choose how to take the loss (buttons below)`
                      : boon
                      ? `${boon.event} — ${boon.canUse ? `tap an in-season ${RESOURCE_LABEL[boon.kind]} card to use it, or decline` : "nothing to use — continue below"}`
                      : swapActive
                      ? `Market Fair — ${swapHandUid ? "tap a market card to take it" : "tap a card to give up"}`
                      : purchase
                      ? `Trading in for a market card — tap any in-season cards (you can pick several)`
                      : buildDraft
                      ? (buildDraft.kind === "layer_set"
                          ? "Composing a layer set — tap same-layer plants to add or remove them"
                          : `Composing a ${buildDraft.biome} forest — tap cards to add or remove them`)
                      : game.pendingGift
                      ? `${game.pendingGift.dir === "right" ? "Generous Neighbor" : "Forager's Gift"} — tap a card, then confirm to pass it to ${game.pendingGift.to}`
                      : myTurn ? "your turn — buy from the market, then take one main action" : "the others are foraging…"}
                  </SmallCaps>
                </div>
                {discard && (
                  <div className={`fg-trade-tally${discard.selected.length === discard.required ? " ok" : ""}`}>
                    <span className="fg-trade-count">
                      {discard.selected.length} of {discard.required} chosen
                    </span>
                    <span className="fg-trade-meter">
                      weevils take <strong>{discard.required}</strong>
                      <span className="fg-trade-need"> {discardModeLabel ? `${discardModeLabel} ` : ""}card{discard.required === 1 ? "" : "s"} — you pick which</span>
                    </span>
                    <span className="fg-trade-hint">
                      {discard.selected.length === discard.required
                        ? "✓ ready — confirm below"
                        : "🍄 mushrooms & guidance are never owed, but you may choose them"}
                    </span>
                  </div>
                )}
                {purchase && (
                  <div className={`fg-trade-tally${payTotal >= purchase.cost ? " ok" : ""}`}>
                    <span className="fg-trade-count">
                      {purchase.selected.length} card{purchase.selected.length === 1 ? "" : "s"} selected
                    </span>
                    <span className="fg-trade-meter">
                      trade-in value <strong>{payTotal}</strong>
                      <span className="fg-trade-need"> / {purchase.cost} needed</span>
                    </span>
                    <span className="fg-trade-hint">
                      {payTotal >= purchase.cost
                        ? "✓ enough — confirm below (extra value is not refunded)"
                        : `tap ${purchase.cost - payTotal} more in value to afford this`}
                    </span>
                  </div>
                )}
                {buildDraft && buildPreview && (
                  <div className={`fg-trade-tally fg-build-tally${buildPreview.valid ? " ok" : ""}`}>
                    <span className="fg-trade-count">
                      {buildDraft.kind === "layer_set"
                        ? `${buildPreview.layerCount} card${buildPreview.layerCount === 1 ? "" : "s"}`
                        : `${buildPreview.layerCount} layer${buildPreview.layerCount === 1 ? "" : "s"}`}
                    </span>
                    <span className="fg-trade-meter">
                      {buildDraft.kind === "layer_set"
                        ? (buildSetLayer ? `${buildSetLayer} layer` : "pick one layer")
                        : (buildLayerNames.length ? buildLayerNames.join(" · ") : "no layers yet")}
                      {buildWildCount > 0 && <span className="fg-trade-need"> + {buildWildCount} wild</span>}
                      <span className="fg-build-score"> · +{buildPreview.score} pts</span>
                    </span>
                    <span className="fg-trade-hint">
                      {buildPreview.valid
                        ? "✓ buildable — confirm below"
                        : (buildPreview.reason || `need ${game.cfg.qualifyingMinLayers}+ layers`)}
                    </span>
                  </div>
                )}
                {!showHand ? (
                  <div className="fg-hand-hidden">
                    {isHumanActive ? "Hand hidden — reveal when you hold the device." : "The bots are foraging…"}
                  </div>
                ) : (
                  <div className="fg-row fg-hand">
                    {viewer.hand.length === 0 && <Notice>Empty hand — draw or take from the market.</Notice>}
                    {viewer.hand.map((c) => {
                      const tv = Forage.tradeValue(c, game.season);
                      const isSelectedToPay = purchase && purchase.selected.includes(c.uid);
                      const cannotPay = purchase && tv <= 0;
                      const isImmune = isImmuneCard(c);
                      // Discard mode: eligible cards are tappable; the rest are safe/locked.
                      const inDiscard = !!discard;
                      const discardSel = discard && discard.selected.includes(c.uid);
                      const discardOk = discard && discardEligible.has(c.uid);
                      // Build mode: for a biome, cards that fit it (or wild
                      // guidance) are tappable; for a layer set, same-layer
                      // plants count (mushrooms & guidance don't).
                      const inBuild = !!buildDraft;
                      const buildSel = buildDraft && buildDraft.selected.includes(c.uid);
                      const buildEligible = buildDraft && (buildDraft.kind === "layer_set"
                        ? (!isImmune && (!buildSetLayer || Forage.effectiveLayer(c) === buildSetLayer))
                        : (Forage.growsIn(c, buildDraft.biome, game.cfg.multiBiome) || isImmune));
                      // Optional boon: only the qualifying resource cards are tappable.
                      const inBoon = !!boon;
                      const boonOk = boon && boonEligible.has(c.uid);
                      // Market Fair swap: pick any hand card to give up.
                      const swapSel = swapActive && c.uid === selUid;
                      const selected =
                        inDiscard ? discardSel
                        : purchase ? isSelectedToPay
                        : inBuild ? buildSel
                        : swapActive ? swapSel
                        : c.uid === selUid;
                      const dim =
                        inDiscard ? (!discardOk && !discardSel)
                        : purchase ? (cannotPay && !isSelectedToPay)
                        : inBuild ? (!buildEligible && !buildSel)
                        : inBoon ? !boonOk
                        : false;
                      let badge = null;
                      if (inDiscard) badge = discardSel ? "✗ discarding" : discardOk ? "tap to drop" : "spared";
                      else if (purchase) badge = tv > 0 ? (isSelectedToPay ? `✓ trading ${tv}` : `tap · ${tv}`) : "can't pay";
                      else if (inBuild) badge = buildSel ? "✓ in build"
                        : buildEligible ? "tap to add"
                        : buildDraft.kind === "layer_set" ? (isImmune ? "🍄 not a set card" : "different layer")
                        : "wrong biome";
                      else if (inBoon) badge = boonOk ? (c.uid === selUid ? "✓ using — confirm below" : "tap to use") : "doesn't qualify";
                      else if (swapActive) badge = swapSel ? "✓ giving up" : "tap to swap";
                      else if (game.pendingGift) badge = c.uid === selUid ? "✓ passing — confirm below" : "tap to pass";
                      return (
                        <ForageCardFace
                          key={c.uid} card={c} season={game.season}
                          selected={selected}
                          dim={dim}
                          cornerBadge={badge}
                          onClick={() =>
                            discard ? toggleDiscard(c.uid)
                            : purchase ? togglePay(c.uid)
                            : buildDraft ? toggleBuildCard(c.uid)
                            // Boon & gift taps only SELECT — the dock's confirm
                            // button commits, so a stray tap can't lose a card.
                            : boon ? (boonOk && setSelUid(c.uid === selUid ? null : c.uid))
                            : swapActive ? setSelUid(c.uid === selUid ? null : c.uid)
                            : setSelUid(c.uid === selUid ? null : c.uid)}
                        />
                      );
                    })}
                  </div>
                )}
              </section>
            </>
          )}
        </main>
      </div>

      {/* Action dock */}
      {!game.over && (
        <footer className="fg-dock">
          {purchase ? (
            <div className="fg-dock-inner" style={{ justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
              <SmallCaps className="accent">🧺 Buying {purchaseCount} market card{purchaseCount === 1 ? "" : "s"}</SmallCaps>
              <span style={{ font: "italic 15px 'EB Garamond', serif", color: "var(--ink)", flex: 1, minWidth: 220 }}>
                {slotMarket && purchaseCount > 1 && <>You'll take the oldest {purchaseCount} cards. </>}
                Trade in in-season hand cards worth at least <strong>{purchase.cost}</strong>.
                <span className="fg-dock-total">
                  Selected{" "}
                  <strong className={payTotal >= purchase.cost ? "ok" : "short"}>{payTotal}</strong>
                  <span className="fg-dock-total-need"> / {purchase.cost}</span>
                </span>
                Mushrooms &amp; guidance can't pay.
              </span>
              <div style={{ display: "flex", gap: 8 }}>
                <button className="hh-btn primary" disabled={payTotal < purchase.cost} onClick={confirmPay}>
                  Confirm trade
                </button>
                <button className="hh-btn ghost" onClick={cancelPay}>Cancel</button>
              </div>
            </div>
          ) : discard ? (
            <div className="fg-dock-inner" style={{ justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
              <SmallCaps className="accent">🐛 {discard.event}</SmallCaps>
              <span style={{ font: "italic 15px 'EB Garamond', serif", color: "var(--ink)", flex: 1, minWidth: 220 }}>
                {discard.tie
                  ? "Your lowest-value cards tie — pick one to let rot."
                  : `Tap ${discardModeLabel ? `${discardModeLabel} ` : ""}cards above to discard.`}
                <span className="fg-dock-total">
                  Chosen{" "}
                  <strong className={discard.selected.length === discard.required ? "ok" : "short"}>{discard.selected.length}</strong>
                  <span className="fg-dock-total-need"> / {discard.required}</span>
                </span>
                {!discard.tie && "Mushrooms & guidance never add to what you owe, but you may choose them."}
              </span>
              <button className="hh-btn primary" disabled={discard.selected.length !== discard.required} onClick={confirmDiscard}>
                Confirm discard
              </button>
            </div>
          ) : buildDraft ? (
            <div className="fg-dock-inner" style={{ justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
              <SmallCaps className="accent">{buildDraft.kind === "layer_set" ? `🌾 Building a${buildSetLayer ? " " + buildSetLayer : ""} layer set` : `🌲 Building ${buildDraft.biome}`}</SmallCaps>
              <span style={{ font: "italic 15px 'EB Garamond', serif", color: "var(--ink)", flex: 1, minWidth: 220 }}>
                Tap cards above to add or remove them.
                <span className="fg-dock-total">
                  <strong className={buildPreview && buildPreview.valid ? "ok" : "short"}>{buildPreview ? buildPreview.layerCount : 0}</strong>
                  <span className="fg-dock-total-need"> {buildDraft.kind === "layer_set" ? "cards" : "layers"} · +{buildPreview ? buildPreview.score : 0} pts</span>
                </span>
                {buildPreview && !buildPreview.valid && <em>{buildPreview.reason}</em>}
              </span>
              <div style={{ display: "flex", gap: 8 }}>
                <button className="hh-btn primary" disabled={!buildPreview || !buildPreview.valid} onClick={confirmBuild}>
                  Confirm build
                </button>
                <button className="hh-btn ghost" onClick={cancelBuild}>Cancel</button>
              </div>
            </div>
          ) : choice ? (
            <div className="fg-dock-inner" style={{ justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
              <SmallCaps className="accent">⚖ {choice.event}</SmallCaps>
              <span style={{ font: "italic 15px 'EB Garamond', serif", color: "var(--ink)", flex: 1, minWidth: 220 }}>
                Take the loss your way: discard one in-season {RESOURCE_LABEL[choice.kind]}-yielding card, or discard {choice.n} cards of your choice.
              </span>
              <div style={{ display: "flex", gap: 8 }}>
                <button className="hh-btn primary" disabled={!choice.canResource} onClick={() => chooseOption("resource")}
                  title={choice.canResource ? "" : `No in-season ${RESOURCE_LABEL[choice.kind]}-yielding card to discard`}>
                  Discard 1 {RESOURCE_LABEL[choice.kind]} card
                </button>
                <button className="hh-btn" disabled={!choice.canBulk} onClick={() => chooseOption("bulk")}
                  title={choice.canBulk ? "" : "No cards to discard"}>
                  Discard {choice.n} cards
                </button>
              </div>
            </div>
          ) : boon ? (
            <div className="fg-dock-inner" style={{ justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
              <SmallCaps className="accent">✨ {boon.event}</SmallCaps>
              <span style={{ font: "italic 15px 'EB Garamond', serif", color: "var(--ink)", flex: 1, minWidth: 220 }}>
                {boon.canUse
                  ? <>Tap an in-season {RESOURCE_LABEL[boon.kind]}-yielding card above, then confirm to discard it and {boon.market ? "take any one market card free" : `draw ${boon.draw}`} — or decline.</>
                  : <>You hold no in-season {RESOURCE_LABEL[boon.kind]}-yielding card — nothing to use here.</>}
              </span>
              {boon.canUse && (
                <button className="hh-btn primary"
                  disabled={!selCard || !boonEligible.has(selCard.uid)}
                  title={selCard && boonEligible.has(selCard.uid) ? `discard ${selCard.name} for the boon` : "select a qualifying card above first"}
                  onClick={() => resolveBoon(true, selCard.uid)}>
                  Use{selCard && boonEligible.has(selCard.uid) ? ` — discard ${selCard.name}` : ""}
                </button>
              )}
              <button className="hh-btn ghost" onClick={() => resolveBoon(false, null)}>
                {boon.canUse ? "Decline" : "Continue"}
              </button>
            </div>
          ) : swap ? (
            <div className="fg-dock-inner" style={{ justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
              <SmallCaps className="accent">🎪 Market Fair</SmallCaps>
              <span style={{ font: "italic 15px 'EB Garamond', serif", color: "var(--ink)", flex: 1, minWidth: 220 }}>
                {swapHandUid
                  ? <>Now tap <strong>Swap here</strong> on a market card to take it for your selected hand card.</>
                  : <>Tap a hand card to give up, then a market card to take — or skip.</>}
              </span>
              <button className="hh-btn ghost" onClick={() => resolveSwap({ skip: true })}>Skip the fair</button>
            </div>
          ) : game.pendingGift ? (
            <div className="fg-dock-inner" style={{ justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
              <SmallCaps className="accent">{game.pendingGift.dir === "right" ? "💞 Generous Neighbor" : "🤝 Forager's Gift"}</SmallCaps>
              <span style={{ font: "italic 15px 'EB Garamond', serif", color: "var(--ink)", flex: 1, minWidth: 220 }}>
                Choose a card from your hand to pass to <strong>{game.pendingGift.to}</strong>
                {game.pendingGift.thenDraw ? <> (you'll then draw {game.pendingGift.thenDraw})</> : null} — tap it above, then confirm.
              </span>
              <button className="hh-btn primary" disabled={!selCard}
                title={selCard ? `pass ${selCard.name} to ${game.pendingGift.to}` : "select a hand card above first"}
                onClick={() => giftCard(selCard.uid)}>
                Pass{selCard ? ` ${selCard.name}` : ""} to {game.pendingGift.to}
              </button>
            </div>
          ) : (
            <div className="fg-dock-inner">
              <div className="fg-dock-builds">
                {builds.length === 0 && myTurn && <SmallCaps className="muted">No build ready — gather more cards.</SmallCaps>}
                {builds.map((b, i) => (
                  <button key={i} className="hh-btn primary" disabled={!myTurn}
                    title={b.kind === "biome"
                      ? `Compose your ${b.biome} forest — choose which cards to play`
                      : b.kind === "layer_set"
                      ? `Compose a layer set — choose which ${b.layer || "same-layer"} plants to play (3+ of one layer)`
                      : `${b.layerCount} cards: ${b.cards.map((c) => c.name).join(", ")}`}
                    onClick={() => openBuild(b)}>
                    {buildLabel(b)} <span className="fg-plus">+{b.score}</span>
                  </button>
                ))}
                {infills.map((it, i) => {
                  const opp = game.players.find((q) => q.seat === it.targetSeat);
                  const f = opp.forests[it.forestIndex];
                  return (
                    <button key={`inf${i}`} className="hh-btn" disabled={!myTurn}
                      style={{ borderStyle: "dashed" }}
                      title={`Community Forests: grow ${opp.name}'s ${f.biome} from ${f.layerCount} to ${it.plan.newLayerCount} layers using ${it.plan.cards.map((c) => c.name).join(", ")}. You score the gain (+${it.plan.score}); ${opp.name} keeps their points and the biome credit.`}
                      onClick={() => act({ type: "build_infill", targetSeat: it.targetSeat, forestIndex: it.forestIndex })}>
                      ⤳ Grow {opp.name}'s {f.biome} <span className="fg-plus">+{it.plan.score}</span>
                    </button>
                  );
                })}
              </div>
              <div className="fg-dock-core">
                <button className="hh-btn" disabled={!myTurn || !selCard || selCard.card_type === "guidance"}
                  title={selCard && selCard.card_type === "guidance" ? "guidance is knowledge, not forage — it can't be cleared" : "discard the selected card"}
                  onClick={() => act({ type: "burn", handUid: selUid })}>
                  Clear{selCard ? ` ${selCard.name}` : ""}
                </button>
                <button className="hh-btn ghost" disabled={!myTurn} onClick={() => act({ type: "pass" })}>Pass</button>
              </div>
            </div>
          )}
        </footer>
      )}

      {inspectSeat != null && game.players[inspectSeat] && (
        <BuildInspector
          player={game.players[inspectSeat]}
          season={game.season}
          onClose={() => setInspectSeat(null)}
        />
      )}

      {recapQueue.length > 0 && (
        <BuildRecap recap={recapQueue[0]} season={game.season} onDismiss={dismissRecap} />
      )}

      {purchase && purchasePopup && (
        <div className="fg-popup-overlay">
          <div className="fg-popup" role="dialog" aria-modal="false">
            <SmallCaps className="accent">🧺 Buying {purchaseCount} card{purchaseCount === 1 ? "" : "s"}</SmallCaps>
            <p className="fg-popup-msg">
              You need to trade in cards with <strong>{purchase.cost}</strong> resource{purchase.cost === 1 ? "" : "s"} this season to buy these cards.
            </p>
            <p className="fg-popup-sub">
              The highlighted cards above are the {purchaseCount} you'll get — the card you picked and every cheaper one below it. Tap in-season cards in your hand worth {purchase.cost} total, then confirm.
            </p>
            <div className="fg-popup-actions">
              <button className="hh-btn primary" onClick={() => setPurchasePopup(false)}>Trade in cards →</button>
              <button className="hh-btn ghost" onClick={cancelPay}>Cancel</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

function isImmuneCard(c) { return c.card_type === "mushroom" || c.card_type === "guidance"; }
function buildActionFor(b) {
  if (b.kind === "biome") return { type: "build_biome", biome: b.biome };
  if (b.kind === "layer_set") return { type: "build_layer_set" };
  if (b.kind === "mushroom_set") return { type: "build_mushroom_set" };
  return { type: "build_guidance_set" };
}
function buildLabel(b) {
  if (b.kind === "biome") return `Build ${biomeForestLabel(b.biome)}`;
  if (b.kind === "layer_set") return b.layer ? `Build ${b.layer} layer set` : "Build layer set";
  if (b.kind === "mushroom_set") return "Play mushroom set";
  return "Complete the Codex";
}

function GameOver({ game, onNew, onInspect }) {
  const ranked = game.players.slice().sort((a, b) => b.finalScore - a.finalScore);
  const winnerPlayers = game.players.filter((p) => game.winners.includes(p.name));
  const humanWon = winnerPlayers.some((p) => p.isHuman);
  const soloYouWon = game.winners.includes("You");
  const headline = soloYouWon
    ? "You win the season."
    : `${game.winners.join(" & ")} ${game.winners.length > 1 ? "win" : "wins"}${humanWon ? "" : " — the bots take it"}.`;
  return (
    <div className="fg-over">
      <SmallCaps>{game.triggeredOn === "biome" ? "Biome diversity reached" : "Points threshold reached"}</SmallCaps>
      <h2 style={{ font: "500 38px/1.1 'EB Garamond', serif", color: "var(--ink)", margin: "6px 0 2px" }}>
        {headline}
      </h2>
      <p style={{ font: "italic 16px 'EB Garamond', serif", color: "var(--ink-soft)", marginTop: 0 }}>
        The wild is well-stocked. Final tally:
      </p>
      <div className="fg-final">
        {ranked.map((p, i) => (
          <button key={p.seat} type="button" className="fg-final-row"
            style={{ borderColor: p.isHuman ? "var(--accent)" : "var(--rule-soft)" }}
            onClick={() => onInspect && onInspect(p.seat)}
            title={p.forests.length ? `View ${p.name}'s built cards` : `${p.name} built nothing`}>
            <span className="fg-final-rank">{i + 1}</span>
            <span className="fg-opp-dot" style={{ background: seatColor(p.seat) }} />
            <span style={{ flex: 1, fontWeight: 600, textAlign: "left" }}>{p.name}{!p.isHuman && " (bot)"}</span>
            <SmallCaps className="muted">{p.biomesBuilt.length} biomes · {p.forests.length} builds{p.forests.length ? " ▸" : ""}</SmallCaps>
            <span className="hh-mono" style={{ fontWeight: 700, fontSize: 18, minWidth: 42, textAlign: "right" }}>{p.finalScore}</span>
          </button>
        ))}
      </div>
      <button className="hh-btn primary xl" style={{ marginTop: 18 }} onClick={onNew}>❦ &nbsp; Play again &nbsp; ❦</button>
    </div>
  );
}

Object.assign(window, { ForageScreen });
