// VELOUR Wardrobe — Step 2: shop-your-closet outfit builder + lifestyle formulas.
// Reads the live closet (passed from DashWardrobe), assembles outfits from owned
// pieces against a formula recipe, flags gaps, and can log a whole look as worn.
const { Card } = window.VelourDesignSystem_380ed4;

const OUTFITS_KEY = (window.VELOUR_FRESH ? "velour_fresh_outfits_v1" : "velour_outfits_v1");

// Lifestyle formulas — each slot accepts pieces from certain categories.
//
// EVERY FORMULA MUST HAVE A DEFINING CATEGORY THE OTHERS DO NOT CLAIM. Smart-Casual
// and Weekend Ease used to read ["Knits","Tops"] and ["Tops","Knits"] — the same set
// of rules written in a different order — so they returned byte-identical outfits for
// every member who ever used them, whatever their closet held. Four buttons, three
// formulas. It looked like a thin closet and was not.
//
// The progression is now structural rather than cosmetic: four pieces, three with a
// knit, three with a top, and a dress. A member who owns no knits gets a gap on
// Smart-Casual, which is the honest answer and the one the gap-filler can act on —
// a silent fallback to a shirt is what hid the duplication in the first place.
// THE NAMES MUST MATCH `AM_FORMULAS` IN src/ui_kits/app/AppWardrobeMore.jsx, CHARACTER FOR
// CHARACTER — see the note there for what happened when they did not. These names are read
// by members, not just rendered: the gap-filler builds sentences out of them.
const FORMULAS = [
  { id: "uniform", name: "The Uniform", note: "Polished, repeatable, hard to get wrong", icon: "coat-hanger",
    slots: [ { label: "Layer", cats: ["Outerwear"] }, { label: "Top", cats: ["Tops"] }, { label: "Bottom", cats: ["Denim"] }, { label: "Shoes", cats: ["Shoes"] } ] },
  { id: "smart", name: "Smart-Casual", note: "Considered, not trying too hard", icon: "sparkle",
    slots: [ { label: "Knit", cats: ["Knits"] }, { label: "Bottom", cats: ["Denim"] }, { label: "Shoes", cats: ["Shoes"] } ] },
  { id: "weekend", name: "Weekend Ease", note: "Comfort that still reads put-together", icon: "sun",
    slots: [ { label: "Top", cats: ["Tops"] }, { label: "Bottom", cats: ["Denim"] }, { label: "Shoes", cats: ["Shoes"] } ] },
  { id: "evening", name: "Evening Out", note: "One step up, for dinners and dates", icon: "moon-stars",
    slots: [ { label: "Dress", cats: ["Dresses"] }, { label: "Layer", cats: ["Outerwear"] }, { label: "Shoes", cats: ["Shoes"] } ],
    // A SECOND RECIPE, used when the closet holds no dress (13 Sep 2026). Evening
    // Out used to be a dress look and nothing else, so a member who dresses up in
    // trousers was shown a bodysuit and mules and told the missing piece was a
    // blazer. Reported from the phone; the missing piece was the trousers.
    // Its defining category, Trousers & skirts, is claimed by no other formula, so
    // it cannot collapse into Weekend Ease — see the rule at the top of this list.
    alt: [ { label: "Top", cats: ["Tops"] }, { label: "Bottom", cats: ["Trousers & skirts"] }, { label: "Shoes", cats: ["Shoes"] } ] },
];

// The recipe this closet gets. Only Evening Out has two, and the rule is as plain
// as it can be: a closet with a dress in it gets the dress look. Resolved once,
// before building or counting, so the outfit, its gap note and the coverage card
// can never be describing different recipes. Mirrored by amResolve() on mobile.
function resolveFormula(formula, items) {
  if (!formula || !formula.alt) return formula;
  const hasDress = items.some((it) => it.category === "Dresses");
  return hasDress ? formula : { ...formula, slots: formula.alt };
}

function cpwVal(it) { return it.wears > 0 ? it.price / it.wears : null; }

// Build an outfit: for each slot pick a matching owned item, rotated by `seed` for variety.
function buildOutfit(formula, items, seed) {
  const used = new Set();
  return formula.slots.map((slot) => {
    const matches = items.filter((it) => slot.cats.includes(it.category) && !used.has(it.id));
    if (!matches.length) return { slot, item: null };
    const pick = matches[seed % matches.length];
    used.add(pick.id);
    return { slot, item: pick };
  });
}

// Would pressing Shuffle actually change anything?
//
// Asked by building the NEXT outfit and comparing, rather than by counting matches
// per slot: `used` means a slot's options depend on what earlier slots took, so a
// count is a guess and this is the real answer. Cheap — the same pure function over
// a handful of items.
//
// It matters because Shuffle is `seed + 1` and each slot picks matches[seed % n].
// When every slot has exactly one match, n is 1, the modulo is always 0, and the
// button silently does nothing forever. A control that promises variety it cannot
// deliver is worse than no control: the member concludes the product is broken
// rather than that their closet is small.
function shuffleWouldChange(formula, items, seed) {
  const a = buildOutfit(formula, items, seed);
  const b = buildOutfit(formula, items, seed + 1);
  return a.some((e, i) => (e.item && e.item.id) !== (b[i].item && b[i].item.id));
}

// Ask the budget planner further down the page to take over this gap. A custom event
// rather than a prop, because the two sections are siblings under DashWardrobe with no
// shared state — and threading a callback through three components to move the page
// would couple them for one interaction.
function requestGapFill(slot) {
  window.dispatchEvent(new CustomEvent("velour:fill-gap", {
    detail: { category: (slot.cats && slot.cats[0]) || null, label: slot.label },
  }));
}

function SlotThumb({ entry }) {
  const { slot, item } = entry;
  if (!item) {
    // A dashed tile with a plus in it promises an action, so it has to have one:
    // it hands the gap to the budget planner below, which is what the note under
    // this look already tells the member to go and do.
    return (
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8, flex: 1, minWidth: 0 }}>
        <button
          onClick={() => requestGapFill(slot)}
          title={`Find a ${slot.label.toLowerCase()} to fill this gap`}
          style={{ width: "100%", aspectRatio: "3 / 4", borderRadius: 12, border: "1.5px dashed var(--amber)", background: "var(--amber-fill)", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 6, padding: 8, textAlign: "center", cursor: "pointer", fontFamily: "var(--font-sans)" }}
        >
          <i className="ph ph-plus-circle" style={{ fontSize: 22, color: "var(--amber-text)" }}></i>
          <span style={{ fontSize: 11, color: "var(--amber-text)", lineHeight: 1.3 }}>Gap — no {slot.label.toLowerCase()}</span>
          <span style={{ fontSize: 10.5, color: "var(--amber-text)", opacity: 0.75, lineHeight: 1.3 }}>Find one</span>
        </button>
        <span style={{ fontSize: 11.5, color: "var(--amber-text)", fontWeight: 500 }}>{slot.label}</span>
      </div>
    );
  }
  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8, flex: 1, minWidth: 0 }}>
      <div style={{ width: "100%", aspectRatio: "3 / 4", borderRadius: 12, overflow: "hidden", background: "var(--amber-fill)", display: "flex", alignItems: "center", justifyContent: "center" }}>
        {item.photo ? <img src={item.photo} alt={item.name} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
          : <i className={`ph ph-${item.icon || "coat-hanger"}`} style={{ fontSize: 30, color: "var(--amber-text)" }}></i>}
      </div>
      <div style={{ width: "100%", textAlign: "center" }}>
        <div style={{ fontSize: 12.5, color: "var(--text-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{item.name}</div>
        <div style={{ fontSize: 11, color: "var(--text-tertiary)", marginTop: 1 }}>{cpwVal(item) != null ? `${window.velourMoney(cpwVal(item))}/wear` : "new"}</div>
      </div>
    </div>
  );
}

// ---- what to add next --------------------------------------------------------
//
// A member who has photographed three pieces opens this section and sees dashed
// boxes in most of the four formulas. That is an honest picture of a thin closet,
// but on its own it reads as the product failing rather than as a closet that is
// three pieces deep — and nothing on the screen says which three photographs would
// change it. Gap tiles hand the gap to the budget planner, which answers "what
// should I buy"; the more common answer at this stage is "you already own one, it
// just is not in here yet."
//
// The advice is DERIVED FROM `FORMULAS`, never a second hand-written list. A slot
// edited above has to move this guidance with it, or the tips will one day ask for
// a knit that no formula wants.
const CATEGORY_ADVICE = {
  Outerwear: "a blazer, coat or jacket",
  Tops: "a shirt or blouse",
  // The second half is the whole point of the line: a closet full of tees reads as a
  // filing error to the member unless the advice says a tee is not what is missing.
  Knits: "a jumper or cardigan, not a jersey tee",
  Denim: "jeans, or a denim skirt",
  Dresses: "a dress",
  "Trousers & skirts": "tailored trousers, or a skirt",
  Shoes: "the pair you reach for most",
};

// Per category: how many pieces the member owns, the most any single formula asks
// for, and which formulas are waiting on it. Counting the *most any one formula
// needs* rather than the total across formulas matters because slots consume from a
// shared pool within a look but not between looks — one pair of shoes completes all
// four formulas, and telling someone to buy four pairs would be nonsense.
// WHAT THIS COUNTS AGAINST IS THE SELECTED FORMULA, and it used to be all four.
//
// `need` was the MAXIMUM any one formula asked for, so the amber chips named
// categories the look on screen does not want — denim and shoes listed as gaps
// while the member is looking at a capsule that needs neither. Reported from the
// phone on 7 September; the same maths was here, so the same fix is. This card sits
// under a formula picker, and everything on it reads as being about the formula
// that is selected.
//
// THE CROSS-FORMULA ANNOTATIONS STAY. `completes` and `neededBy` are still computed
// across all four, because "also completes Evening Out" is a true and useful thing
// to say about a piece the SELECTED look needs. Sourcing the LIST from all four was
// the error; describing its entries in terms of all four is the point.
function coverageReport(items, formula) {
  const owned = {};
  items.forEach((it) => { owned[it.category] = (owned[it.category] || 0) + 1; });

  const need = {};      // category -> what the SELECTED formula asks for
  const completes = {}; // category -> formulas this ALONE would finish
  const neededBy = {};  // category -> formulas that want it but need more besides
  if (formula) {
    formula.slots.forEach((s) => { s.cats.forEach((c) => { need[c] = (need[c] || 0) + 1; }); });
  }
  FORMULAS.map((f) => resolveFormula(f, items)).forEach((f) => {
    const perFormula = {};
    f.slots.forEach((s) => { s.cats.forEach((c) => { perFormula[c] = (perFormula[c] || 0) + 1; }); });
    const short = Object.keys(perFormula).filter((c) => (owned[c] || 0) < perFormula[c]);
    if (!formula) Object.keys(perFormula).forEach((c) => { need[c] = Math.max(need[c] || 0, perFormula[c]); });
    // "Completes Evening Out" is a promise, and it is only true when this category
    // is the formula's ONLY shortfall. A member two categories short who adds the
    // one we named and watches the look stay broken has been told something false
    // by the part of the screen whose whole job is to be trustworthy about gaps.
    short.forEach((c) => {
      if (short.length === 1) (completes[c] = completes[c] || []).push(f.name);
      else (neededBy[c] = neededBy[c] || []).push(f.name);
    });
  });

  const missing = Object.keys(need)
    .filter((c) => (owned[c] || 0) < need[c])
    .map((c) => ({ cat: c, have: owned[c] || 0, need: need[c], completes: completes[c] || [], neededBy: neededBy[c] || [] }))
    .sort((a, b) => (b.completes.length - a.completes.length) || (b.neededBy.length - a.neededBy.length));

  // Owned, but only just: one piece in a slot means Shuffle has nowhere to go.
  const thin = Object.keys(need).filter((c) => (owned[c] || 0) === need[c]).sort();

  const complete = FORMULAS.map((f) => resolveFormula(f, items)).filter((f) => {
    const perFormula = {};
    f.slots.forEach((s) => { s.cats.forEach((c) => { perFormula[c] = (perFormula[c] || 0) + 1; }); });
    return Object.keys(perFormula).every((c) => (owned[c] || 0) >= perFormula[c]);
  }).length;

  return { missing, thin, complete, total: FORMULAS.length };
}

const NUM_WORD = ["No", "One", "Two", "Three", "Four", "Five", "Six"];

// One reading measure for the card. The amber rows are why it exists: at a wide
// desktop width a one-line row stretched its fill nearly the width of the page, so
// the colour read as a banner across the section rather than as a note inside it.
// Text-only blocks share it so the left edge stays a single column.
const COVERAGE_MEASURE = 760;

function CapsuleCoverage({ items, formula }) {
  const { missing, thin, complete, total } = coverageReport(items, formula);
  const [openTips, setOpenTips] = React.useState(false);
  // Nothing missing and every slot has room to vary — the member needs no advice.
  if (!missing.length && !thin.length) return null;

  const headline = complete === total
    ? "All four formulas are complete."
    : complete === 0
      ? "None of the four formulas is complete yet."
      : `${NUM_WORD[complete]} of ${NUM_WORD[total].toLowerCase()} formulas ${complete === 1 ? "is" : "are"} complete.`;

  return (
    <div style={{ background: "var(--surface-card)", border: "var(--border-width) solid var(--border)", borderRadius: 16, padding: "22px 24px", marginBottom: 20, boxShadow: "var(--shadow-sm)" }}>
      <div style={{ fontSize: 11, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--amber-text)", fontWeight: 500 }}>What to add next</div>
      <div style={{ fontFamily: "var(--font-serif)", fontSize: 20, color: "var(--text-primary)", letterSpacing: "-0.01em", margin: "7px 0 6px" }}>{headline}</div>
      {/* This line explains gaps, so it only belongs where there are gaps. Shown
          under "All four formulas are complete." it contradicted the headline
          immediately above it. */}
      <p style={{ fontSize: 14, color: "var(--text-secondary)", margin: 0, lineHeight: 1.6, maxWidth: COVERAGE_MEASURE }}>
        {missing.length > 0
          ? `${formula.name} asks for a particular kind of piece. Most gaps here are pieces you already own and have not photographed yet.`
          : "Every formula has something to work with. Depth is what turns four looks into a rotation."}
      </p>

      {missing.length > 0 && (
        <div style={{ display: "flex", flexDirection: "column", gap: 9, marginTop: 16, maxWidth: COVERAGE_MEASURE }}>
          {missing.map((m) => (
            <div key={m.cat} style={{ display: "flex", gap: 11, alignItems: "flex-start", background: "var(--amber-fill)", borderRadius: 12, padding: "12px 15px" }}>
              <i className="ph ph-camera" style={{ fontSize: 16, color: "var(--amber)", marginTop: 2 }}></i>
              <div style={{ fontSize: 13.5, color: "var(--amber-text)", lineHeight: 1.55 }}>
                <strong style={{ fontWeight: 600 }}>{m.need > 1 ? `${m.need} ${m.cat.toLowerCase()}` : m.cat}</strong>
                {" — "}{CATEGORY_ADVICE[m.cat] || `a piece filed under ${m.cat.toLowerCase()}`}.
                {m.completes.length > 0 && <span style={{ opacity: 0.85 }}>{" "}Completes {m.completes.join(" and ")}.</span>}
                {m.neededBy.length > 0 && <span style={{ opacity: 0.85 }}>{" "}Also needed for {m.neededBy.join(" and ")}.</span>}
              </div>
            </div>
          ))}
        </div>
      )}

      {thin.length > 0 && (
        <p style={{ fontSize: 13.5, color: "var(--text-secondary)", margin: "14px 0 0", lineHeight: 1.6, maxWidth: COVERAGE_MEASURE }}>
          You own exactly one piece in {thin.map((c) => c.toLowerCase()).join(", ")}. A second gives Shuffle somewhere to go, and the looks start to vary.
        </p>
      )}

      <button onClick={() => setOpenTips((v) => !v)} style={{ display: "inline-flex", alignItems: "center", gap: 7, background: "none", border: "none", padding: 0, marginTop: 16, font: "inherit", fontSize: 13.5, fontWeight: 500, color: "var(--text-primary)", cursor: "pointer" }}>
        <i className={`ph ph-caret-${openTips ? "down" : "right"}`} style={{ fontSize: 13 }}></i>
        Photos that read well, and which files work
      </button>

      {openTips && (
        <div style={{ marginTop: 12, paddingTop: 14, borderTop: "var(--border-width) solid var(--divider)", maxWidth: COVERAGE_MEASURE }}>
          {/* Three rules in the order the evidence says they matter — docs/WARDROBE_PHOTO_STANDARD.md */}
          <ul style={{ margin: 0, paddingLeft: 18, display: "flex", flexDirection: "column", gap: 6 }}>
            <li style={{ fontSize: 13.5, color: "var(--text-secondary)", lineHeight: 1.55 }}>Lay the piece flat and the right way up, on a plain surface.</li>
            <li style={{ fontSize: 13.5, color: "var(--text-secondary)", lineHeight: 1.55 }}>One piece per photo — glance at the edges of the frame.</li>
            <li style={{ fontSize: 13.5, color: "var(--text-secondary)", lineHeight: 1.55 }}>Any phone camera is enough. VELOUR resizes every photo itself.</li>
            {/* The one real format limit: the browser has to decode the file before we
                can compress it, and Chrome, Firefox and Android cannot decode HEIC.
                The piece still saves — the photo is what goes missing. */}
            <li style={{ fontSize: 13.5, color: "var(--text-secondary)", lineHeight: 1.55 }}>JPG and PNG always work. An iPhone HEIC converts itself when you upload from the iPhone, but on a computer or an Android phone it may not read — export it as JPG first.</li>
          </ul>
          <p style={{ fontSize: 13.5, color: "var(--text-secondary)", margin: "12px 0 0", lineHeight: 1.6 }}>Thirty to forty pieces is a capsule. Start with what you actually reach for.</p>
        </div>
      )}
    </div>
  );
}

function WardrobeOutfits({ items, onWearMany }) {
  const [active, setActive] = React.useState("uniform");
  const [seed, setSeed] = React.useState(0);
  const [saved, setSaved] = React.useState(() => { try { return JSON.parse(localStorage.getItem(OUTFITS_KEY)) || []; } catch (e) { return []; } });
  React.useEffect(() => { try { localStorage.setItem(OUTFITS_KEY, JSON.stringify(saved)); } catch (e) {} }, [saved]);
  // Saved looks arrive with the rest of the wardrobe on a second device.
  if (window.useVelourSynced) window.useVelourSynced("wardrobe", () => {
    try { setSaved(JSON.parse(localStorage.getItem(OUTFITS_KEY)) || []); } catch (e) {}
  });

  const formula = resolveFormula(FORMULAS.find((f) => f.id === active), items);
  const outfit = buildOutfit(formula, items, seed);
  const canShuffle = shuffleWouldChange(formula, items, seed);
  const filled = outfit.filter((e) => e.item);
  const gaps = outfit.length - filled.length;
  const outfitCpw = filled.reduce((s, e) => s + (cpwVal(e.item) || 0), 0);
  const allOwned = gaps === 0;

  const wearLook = () => { onWearMany(filled.map((e) => e.item.id)); };
  // Every piece in this look already worn today. `wornToday` is DashWardrobe.jsx's
  // helper, global here because the web entry loads each screen as its own classic
  // script — the mobile build wraps its screens in IIFEs instead, which is why the
  // same change there needed an explicit window export.
  const outfitWorn = filled.length > 0 && filled.every((e) => wornToday(e.item));
  const saveLook = () => {
    const look = { id: "o" + Math.random().toString(36).slice(2, 7), formula: formula.name, itemIds: filled.map((e) => e.item.id), names: filled.map((e) => e.item.name) };
    if (!saved.some((s) => s.itemIds.join() === look.itemIds.join())) setSaved([look, ...saved].slice(0, 6));
  };
  const removeSaved = (id) => setSaved(saved.filter((s) => s.id !== id));

  return (
    <div>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12, margin: "8px 2px 16px" }}>
        <div>
          <h2 style={{ fontFamily: "var(--font-serif)", fontWeight: 500, fontSize: 24, color: "var(--text-primary)", margin: 0, letterSpacing: "-0.01em" }}>Shop your capsule</h2>
          <p style={{ fontSize: 14, color: "var(--text-secondary)", margin: "6px 0 0" }}>Pick a formula — VELOUR builds the look from pieces you already own.</p>
        </div>
      </div>

      {/* Formula selector */}
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 170px), 1fr))", gap: 12, marginBottom: 20 }}>
        {FORMULAS.map((f) => {
          const on = active === f.id;
          return (
            <button key={f.id} onClick={() => { setActive(f.id); setSeed(0); }} style={{ textAlign: "left", cursor: "pointer", borderRadius: 16, padding: "16px 18px", border: "var(--border-width) solid " + (on ? "var(--amber)" : "var(--border)"), background: on ? "var(--amber-fill)" : "var(--surface-card)", boxShadow: on ? "none" : "var(--shadow-sm)", fontFamily: "var(--font-sans)", transition: "all .15s var(--ease-out)" }}>
              <i className={`ph ph-${f.icon}`} style={{ fontSize: 20, color: on ? "var(--amber)" : "var(--text-tertiary)" }}></i>
              <div style={{ fontFamily: "var(--font-serif)", fontSize: 18, color: "var(--text-primary)", letterSpacing: "-0.01em", marginTop: 10 }}>{f.name}</div>
              <div style={{ fontSize: 12.5, color: "var(--text-tertiary)", marginTop: 4, lineHeight: 1.45 }}>{f.note}</div>
            </button>
          );
        })}
      </div>

      <CapsuleCoverage items={items} formula={formula} />

      {/* Built outfit */}
      <Card padding={26}>
        <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", flexWrap: "wrap", gap: 16, marginBottom: 22 }}>
          <div>
            <div style={{ fontSize: 11, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--amber-text)", fontWeight: 500 }}>{formula.name}</div>
            <div style={{ fontFamily: "var(--font-serif)", fontSize: 22, color: "var(--text-primary)", letterSpacing: "-0.01em", marginTop: 5 }}>
              {allOwned ? "Built entirely from your capsule" : `${filled.length} of ${outfit.length} from your capsule`}
            </div>
          </div>
          <div style={{ textAlign: "right" }}>
            <div style={{ fontFamily: "var(--font-serif)", fontSize: 24, color: "var(--text-primary)" }}>{window.velourMoney(outfitCpw)}</div>
            <div style={{ fontSize: 12, color: "var(--text-tertiary)" }}>this outfit · per wear</div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 16, alignItems: "flex-start" }}>
          {outfit.map((entry, i) => (
            <React.Fragment key={i}>
              <SlotThumb entry={entry} />
              {i < outfit.length - 1 && <div style={{ alignSelf: "center", color: "var(--text-tertiary)", fontSize: 18, paddingTop: 18 }}>+</div>}
            </React.Fragment>
          ))}
        </div>

        {gaps > 0 && (
          <div style={{ display: "flex", gap: 11, alignItems: "flex-start", background: "var(--amber-fill)", borderRadius: 13, padding: "14px 16px", marginTop: 20 }}>
            <i className="ph ph-lightbulb" style={{ fontSize: 18, color: "var(--amber)", marginTop: 1 }}></i>
            <div style={{ fontSize: 14, color: "var(--amber-text)", lineHeight: 1.55 }}>
              {gaps === 1 ? "This look is one piece short." : `This look is ${gaps} pieces short.`} VELOUR can suggest the exact gap-filler worth buying — never a duplicate of what you own.
              {" "}
              <button
                onClick={() => { const first = outfit.find((e) => !e.item); if (first) requestGapFill(first.slot); }}
                style={{ background: "none", border: "none", padding: 0, font: "inherit", color: "var(--amber-text)", fontWeight: 600, textDecoration: "underline", textUnderlineOffset: 3, cursor: "pointer" }}
              >Find the gap-filler</button>
            </div>
          </div>
        )}

        <div style={{ display: "flex", gap: 10, marginTop: 22, flexWrap: "wrap" }}>
          {/* Mirrors the closet card in DashWardrobe.jsx, which already flips on
              wornToday. Reported on mobile, but the web button had the same gap:
              a permanent check icon and a label that never acknowledged the tap. */}
          <button onClick={wearLook} disabled={!filled.length} style={{ display: "inline-flex", alignItems: "center", gap: 8, background: outfitWorn ? "var(--amber-fill)" : "var(--ink)", color: outfitWorn ? "var(--amber-text)" : "var(--parchment)", border: "none", borderRadius: 999, padding: "11px 20px", fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 500, cursor: "pointer", opacity: filled.length ? 1 : 0.5 }}><i className={`ph ph-${outfitWorn ? "check" : "plus"}`} style={{ fontSize: 15 }}></i>{outfitWorn ? "Worn today" : "Wear this today"}</button>
          <button onClick={() => setSeed((s) => s + 1)} disabled={!canShuffle} title={canShuffle ? undefined : "Every slot in this look has one match"} style={{ display: "inline-flex", alignItems: "center", gap: 8, background: "var(--surface-card)", color: "var(--text-primary)", border: "var(--border-width) solid var(--border)", borderRadius: 999, padding: "11px 20px", fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 500, cursor: canShuffle ? "pointer" : "default", opacity: canShuffle ? 1 : 0.5 }}><i className="ph ph-shuffle" style={{ fontSize: 15 }}></i>Shuffle</button>
          <button onClick={saveLook} style={{ display: "inline-flex", alignItems: "center", gap: 8, background: "var(--surface-card)", color: "var(--text-primary)", border: "var(--border-width) solid var(--border)", borderRadius: 999, padding: "11px 20px", fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 500, cursor: "pointer" }}><i className="ph ph-bookmark-simple" style={{ fontSize: 15 }}></i>Save look</button>
        </div>

        {!canShuffle && filled.length > 0 && (
          <div style={{ fontSize: 13, color: "var(--text-tertiary)", lineHeight: 1.6, marginTop: 12 }}>
            Every slot in this look has one match, so there is nothing to shuffle to yet. Add a second top, bottom or pair of shoes and the looks will start to vary.
          </div>
        )}
      </Card>

      {/* Saved looks */}
      {saved.length > 0 && (
        <div style={{ marginTop: 20 }}>
          <div style={{ fontSize: 11, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--text-tertiary)", fontWeight: 500, margin: "0 2px 12px" }}>Saved looks</div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 200px), 1fr))", gap: 12 }}>
            {saved.map((look) => (
              <div key={look.id} className="vr-item" style={{ position: "relative", background: "var(--surface-card)", border: "var(--border-width) solid var(--border)", borderRadius: 14, padding: "14px 16px", boxShadow: "var(--shadow-sm)" }}>
                <button onClick={() => removeSaved(look.id)} aria-label="Remove look" className="vr-item-actions" style={{ position: "absolute", top: 10, right: 10, width: 26, height: 26, borderRadius: "50%", border: "none", background: "var(--surface-sunken)", color: "var(--text-secondary)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><i className="ph ph-x" style={{ fontSize: 12 }}></i></button>
                <div style={{ fontSize: 11, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--amber-text)", fontWeight: 500 }}>{look.formula}</div>
                <div style={{ fontSize: 13.5, color: "var(--text-secondary)", marginTop: 8, lineHeight: 1.5 }}>{look.names.join(" · ")}</div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

window.WardrobeOutfits = WardrobeOutfits;
