const BASE_WIDTH = 1280;
const BASE_HEIGHT = 720;

const START_MINUTE = 10 * 60 + 30;
const OPEN_MINUTE = 11 * 60;
const CLOSE_MINUTE = 20 * 60;
const DAY_END_MINUTE = 20 * 60 + 30;
const MINUTES_PER_SECOND = 7;
const SAVE_STORAGE_KEY = "madecafe-save-slot-1";
const SAVE_DATA_VERSION = 1;
const MAX_INVENTORY_AMOUNT = 99;
const MAX_MAID_STAT = 20;
let dayStartCheckpoint = null;
const ASSETS = {
  ui: {
    thankYouForPlaying: "assets/ui/thank_you_for_playing.png",
  },
  maids: {
    "maid-1": "assets/characters/maids/kotone_idle.png",
    "maid-2": "assets/characters/maids/suzu_idle.png",
    "maid-3": "assets/characters/maids/hina_idle.png",
  },
  customers: {
    "office-worker": "assets/characters/customers/customer_01.png",
    student: "assets/characters/customers/customer_02.png",
    otaku: "assets/characters/customers/customer_03.png",
    couple: "assets/characters/customers/customer_04.png",
  },
  ingredients: {
    milk: "assets/ingredients/milk.png",
    tea: "assets/ingredients/tea.png",
    coffee: "assets/ingredients/coffee.png",
    sugar: "assets/ingredients/sugar.png",
    bread: "assets/ingredients/bread.png",
    rice: "assets/ingredients/rice.png",
    pasta: "assets/ingredients/pasta.png",
    egg: "assets/ingredients/egg.png",
    meat: "assets/ingredients/meat.png",
    flour: "assets/ingredients/flour.png",
    vegetable: "assets/ingredients/vegetable.png",
    fruit: "assets/ingredients/fruit.png",
    chocolate: "assets/ingredients/chocolate.png",
    iceCream: "assets/ingredients/ice_cream.png",
    unknown: "assets/ingredients/unknown_ingredient.png",
  },
  foods: {
    tea: "assets/foods/tea.png",
    "black-coffee": "assets/foods/black_coffee.png",
    "hot-milk": "assets/foods/hot_milk.png",
    "milk-tea": "assets/foods/milk_tea.png",
    "cafe-au-lait": "assets/foods/cafe_au_lait.png",
    "fruit-tea": "assets/foods/fruit_tea.png",
    "hot-choco-milk": "assets/foods/hot_choco_milk.png",
    "fruit-milk": "assets/foods/fruit_milk.png",
    omurice: "assets/foods/omurice.png",
    "fried-rice": "assets/foods/fried_rice.png",
    "tomato-pasta": "assets/foods/tomato_pasta.png",
    carbonara: "assets/foods/carbonara.png",
    "hamburg-set": "assets/foods/hamburg_set.png",
    pancake: "assets/foods/pancake.png",
    pudding: "assets/foods/pudding.png",
    "choco-pudding": "assets/foods/choco_pudding.png",
    "special-pancake": "assets/foods/special_pancake.png",
    "chiffon-cake": "assets/foods/chiffon_cake.png",
    "gateau-chocolat": "assets/foods/gateau_chocolat.png",
    "choco-banana-crepe": "assets/foods/choco_banana_crepe.png",
    "choco-ice-pancake": "assets/foods/choco_ice_pancake.png",
    "choco-banana-ice-crepe": "assets/foods/choco_banana_ice_crepe.png",
    "fruit-ice-parfait": "assets/foods/fruit_ice_parfait.png",
    "pudding-a-la-mode": "assets/foods/pudding_a_la_mode.png",
    "egg-sand": "assets/foods/egg_sand.png",
    sandwich: "assets/foods/sandwich.png",
    hamburger: "assets/foods/hamburger.png",
    "french-toast": "assets/foods/french_toast.png",
    oyakodon: "assets/foods/oyakodon.png",
    emptyPlate: "assets/foods/empty_plate.png",
    unknown: "assets/foods/unknown_food.png",
  },
  adv: {
    backgrounds: {
      // 旧IDの互換用。cafe_day/cafe_night素材は使わず、現行のホール背景へ向けます。
      cafeDay: "assets/adv/backgrounds/cafe_hall_day.png",
      cafeNight: "assets/adv/backgrounds/cafe_hall_night.png",
      cafeFrontDay: "assets/adv/backgrounds/cafe_front_day.png",
      cafeFrontNight: "assets/adv/backgrounds/cafe_front_night.png",
      cafeFrontClose: "assets/adv/backgrounds/cafe_front_close.png",
      cafeHallDay: "assets/adv/backgrounds/cafe_hall_day.png",
      cafeHallNight: "assets/adv/backgrounds/cafe_hall_night.png",
      cafeHallClose: "assets/adv/backgrounds/cafe_hall_close.png",
      kitchenDay: "assets/adv/backgrounds/kitchen_day.png",
      backyardNight: "assets/adv/backgrounds/backyard_night.png",
      lawOfficeDay: "assets/adv/backgrounds/law_office_day.png",
      stationDay: "assets/adv/backgrounds/station_day.png",
      cityCafeDay: "assets/adv/backgrounds/city_cafe_day.png",
      fountainEvening: "assets/adv/backgrounds/fountain_evening.png",
      prepRoom: "assets/adv/backgrounds/cafe_hall_day.png",
      black: "assets/adv/backgrounds/black.png",
      white: "assets/adv/backgrounds/white.png",
    },
    eventCg: {
      main01: "assets/adv/cg/main/cg_main_01.png",
    },
    portraits: {
      "maid-1": {
        default: "assets/adv/portraits/kotone/kotone_default.png",
        smile: "assets/adv/portraits/kotone/kotone_smile.png",
        soft_smile: "assets/adv/portraits/kotone/kotone_soft_smile.png",
        closed_smile: "assets/adv/portraits/kotone/kotone_closed_smile.png",
        serious: "assets/adv/portraits/kotone/kotone_serious.png",
        angry: "assets/adv/portraits/kotone/kotone_angry.png",
        blush: "assets/adv/portraits/kotone/kotone_blush.png",
        crying: "assets/adv/portraits/kotone/kotone_crying.png",
        sad: "assets/adv/portraits/kotone/kotone_sad.png",
        shy: "assets/adv/portraits/kotone/kotone_shy.png",
        silent: "assets/adv/portraits/kotone/kotone_silent.png",
        surprised: "assets/adv/portraits/kotone/kotone_surprised.png",
        teary: "assets/adv/portraits/kotone/kotone_teary.png",
        troubled: "assets/adv/portraits/kotone/kotone_troubled.png",
        worried: "assets/adv/portraits/kotone/kotone_worried.png",
      },
      "maid-2": {
        default: "assets/adv/portraits/suzu/suzu_default.png",
        smile: "assets/adv/portraits/suzu/suzu_smile.png",
        soft_smile: "assets/adv/portraits/suzu/suzu_soft_smile.png",
        closed_smile: "assets/adv/portraits/suzu/suzu_closed_smile.png",
        serious: "assets/adv/portraits/suzu/suzu_serious.png",
        angry: "assets/adv/portraits/suzu/suzu_angry.png",
        blush: "assets/adv/portraits/suzu/suzu_blush.png",
        crying: "assets/adv/portraits/suzu/suzu_crying.png",
        sad: "assets/adv/portraits/suzu/suzu_sad.png",
        shy: "assets/adv/portraits/suzu/suzu_shy.png",
        silent: "assets/adv/portraits/suzu/suzu_silent.png",
        surprised: "assets/adv/portraits/suzu/suzu_surprised.png",
        teary: "assets/adv/portraits/suzu/suzu_teary.png",
        troubled: "assets/adv/portraits/suzu/suzu_troubled.png",
        worried: "assets/adv/portraits/suzu/suzu_worried.png",
      },
      "maid-3": {
        default: "assets/adv/portraits/hina/hina_default.png",
        smile: "assets/adv/portraits/hina/hina_smile.png",
        soft_smile: "assets/adv/portraits/hina/hina_soft_smile.png",
        closed_smile: "assets/adv/portraits/hina/hina_closed_smile.png",
        serious: "assets/adv/portraits/hina/hina_serious.png",
        angry: "assets/adv/portraits/hina/hina_angry.png",
        blush: "assets/adv/portraits/hina/hina_blush.png",
        crying: "assets/adv/portraits/hina/hina_crying.png",
        sad: "assets/adv/portraits/hina/hina_sad.png",
        shy: "assets/adv/portraits/hina/hina_shy.png",
        silent: "assets/adv/portraits/hina/hina_silent.png",
        surprised: "assets/adv/portraits/hina/hina_surprised.png",
        teary: "assets/adv/portraits/hina/hina_teary.png",
        troubled: "assets/adv/portraits/hina/hina_troubled.png",
        worried: "assets/adv/portraits/hina/hina_worried.png",
      },
    },
  },
};

const TEXT = {
  yen: "円",
  daySuffix: "日目",
  empty: "空席",
  seated: "注文待ち",
  noPreference: "う～ん",
  cooking: "を調理中",
  waitServe: "まだかな",
  eating: "食事中",
  finished: "休憩中",
  dirty: "片付け待ち",
  cleaned: "片付け完了",
  idle: "待機中",
  looking: "待機中",
  closingWork: "閉店処理中",
  servingHall: "ホール待機中",
  maidPrefix: "メイド",
  takeOrderBubble: "ご注文は？",
  cookBubble: "調理中",
  serveBubble: "お待たせしました",
  cleanBubble: "片付けます",
  openingSoon: "11:00 開店予定",
  waitingGuest: "お客さん待ち",
  closingGuests: "残りのお客さんを対応中",
  dayFinished: "1日の営業が完了しました",
  nextDayReady: "翌日の開店準備へ移行中",
  opening: "開店準備",
  open: "営業中",
  closing: "閉店処理",
  closed: "営業終了",
  phase: {
    arrive: "来店フェーズ",
    order: "注文確認フェーズ",
    cook: "調理フェーズ",
    serve: "配膳フェーズ",
    eat: "食事フェーズ",
    leave: "退店フェーズ",
    clean: "片付けフェーズ",
    sales: "会計フェーズ",
  },
};

const MENUS = [
  {
    id: "tea",
    name: "紅茶",
    genre: "drink",
    phase: "initial",
    recommendedReputation: 0,
    price: 460,
    cookTime: 9,
    icon: getMenuIcon("tea"),
    unlocked: true,
    isPublished: true,
    popularity: 3,
    ingredients: { tea: 2 },
  },
  {
    id: "black-coffee",
    name: "ブラックコーヒー",
    genre: "drink",
    phase: "initial",
    recommendedReputation: 0,
    price: 440,
    cookTime: 8,
    icon: getMenuIcon("black-coffee"),
    unlocked: true,
    isPublished: true,
    popularity: 3,
    ingredients: { coffee: 2 },
  },
  {
    id: "hot-milk",
    name: "ホットミルク",
    genre: "drink",
    phase: "initial",
    recommendedReputation: 0,
    price: 450,
    cookTime: 8,
    icon: getMenuIcon("hot-milk"),
    unlocked: true,
    isPublished: true,
    popularity: 3,
    ingredients: { milk: 2 },
  },
  {
    id: "milk-tea",
    name: "ミルクティー",
    genre: "drink",
    phase: "initial",
    recommendedReputation: 0,
    price: 600,
    cookTime: 9,
    icon: getMenuIcon("milk-tea"),
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { tea: 1, sugar: 1, milk: 1 },
  },
  {
    id: "egg-sand",
    name: "たまごサンド",
    genre: "meal",
    phase: "mid",
    recommendedReputation: 15,
    price: 980,
    cookTime: 15,
    icon: getMenuIcon("egg-sand"),
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { bread: 1, egg: 1 },
  },
  {
    id: "sandwich",
    name: "サンドイッチ",
    genre: "meal",
    phase: "mid",
    recommendedReputation: 20,
    price: 1180,
    cookTime: 17,
    icon: getMenuIcon("sandwich"),
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { bread: 1, egg: 1, vegetable: 1 },
  },
  {
    id: "hamburger",
    name: "ハンバーガー",
    genre: "meal",
    phase: "late",
    recommendedReputation: 40,
    price: 1680,
    cookTime: 24,
    icon: getMenuIcon("hamburger"),
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { bread: 1, meat: 1, vegetable: 1 },
  },
  {
    id: "french-toast",
    name: "フレンチトースト",
    genre: "dessert",
    phase: "mid",
    recommendedReputation: 30,
    price: 1320,
    cookTime: 17,
    icon: getMenuIcon("french-toast"),
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { bread: 1, milk: 1, sugar: 1, egg: 1 },
  },
  {
    id: "oyakodon",
    name: "親子丼",
    genre: "meal",
    phase: "late",
    recommendedReputation: 40,
    price: 1380,
    cookTime: 22,
    icon: getMenuIcon("oyakodon"),
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { rice: 1, egg: 1, meat: 1 },
  },
  {
    id: "cafe-au-lait",
    name: "カフェオレ",
    genre: "drink",
    phase: "mid",
    recommendedReputation: 6,
    price: 620,
    cookTime: 9,
    icon: getMenuIcon("cafe-au-lait"),
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { coffee: 1, milk: 1, sugar: 1 },
  },
  {
    id: "fruit-tea",
    name: "フルーツティー",
    genre: "drink",
    phase: "mid",
    recommendedReputation: 8,
    price: 720,
    cookTime: 10,
    icon: getMenuIcon("fruit-tea"),
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { tea: 1, sugar: 1, fruit: 1 },
  },
  {
    id: "hot-choco-milk",
    name: "ホットチョコミルク",
    genre: "drink",
    phase: "mid",
    recommendedReputation: 10,
    price: 860,
    cookTime: 10,
    icon: getMenuIcon("hot-choco-milk"),
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { milk: 1, chocolate: 1, sugar: 1 },
  },
  {
    id: "fruit-milk",
    name: "フルーツミルク",
    genre: "drink",
    phase: "mid",
    recommendedReputation: 10,
    price: 780,
    cookTime: 10,
    icon: getMenuIcon("fruit-milk"),
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { milk: 1, fruit: 1, sugar: 1 },
  },
  {
    id: "omurice",
    name: "オムライス",
    genre: "meal",
    phase: "initial",
    recommendedReputation: 0,
    price: 1000,
    cookTime: 21,
    icon: ASSETS.foods.omurice,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { rice: 1, egg: 2 },
  },
  {
    id: "fried-rice",
    name: "チャーハン",
    genre: "meal",
    phase: "mid",
    recommendedReputation: 40,
    price: 1420,
    cookTime: 24,
    icon: ASSETS.foods.omurice,
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { rice: 1, meat: 1, vegetable: 1 },
  },
  {
    id: "tomato-pasta",
    name: "トマトパスタ",
    genre: "meal",
    phase: "mid",
    recommendedReputation: 45,
    price: 1520,
    cookTime: 26,
    icon: ASSETS.foods.omurice,
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { pasta: 1, vegetable: 1, meat: 1 },
  },
  {
    id: "carbonara",
    name: "カルボナーラ",
    genre: "meal",
    phase: "mid",
    recommendedReputation: 45,
    price: 1280,
    cookTime: 23,
    icon: ASSETS.foods.omurice,
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { pasta: 1, egg: 1, milk: 1 },
  },
  {
    id: "hamburg-set",
    name: "ハンバーグセット",
    genre: "meal",
    phase: "late",
    recommendedReputation: 50,
    price: 1920,
    cookTime: 31,
    icon: ASSETS.foods.omurice,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { rice: 1, meat: 1, egg: 1, vegetable: 1 },
  },
  {
    id: "pancake",
    name: "パンケーキ",
    genre: "dessert",
    phase: "mid",
    recommendedReputation: 10,
    price: 980,
    cookTime: 15,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { flour: 1, egg: 1, milk: 1, sugar: 1 },
  },
  {
    id: "pudding",
    name: "プリン",
    genre: "dessert",
    phase: "initial",
    recommendedReputation: 0,
    price: 820,
    cookTime: 13,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { egg: 1, milk: 1, sugar: 1 },
  },
  {
    id: "choco-pudding",
    name: "チョコプリン",
    genre: "dessert",
    phase: "mid",
    recommendedReputation: 55,
    price: 1180,
    cookTime: 16,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 4,
    ingredients: { egg: 1, milk: 1, sugar: 1, chocolate: 1 },
  },
  {
    id: "special-pancake",
    name: "スペシャルパンケーキ",
    genre: "dessert",
    phase: "mid",
    recommendedReputation: 30,
    price: 1460,
    cookTime: 20,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { flour: 1, egg: 1, milk: 1, sugar: 1, fruit: 1 },
  },
  {
    id: "choco-ice-pancake",
    name: "チョコアイスパンケーキ",
    genre: "dessert",
    phase: "late",
    recommendedReputation: 70,
    price: 1820,
    cookTime: 23,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { flour: 1, egg: 1, milk: 1, sugar: 1, chocolate: 1, iceCream: 1 },
  },
  {
    id: "chiffon-cake",
    name: "シフォンケーキ",
    genre: "dessert",
    phase: "mid",
    recommendedReputation: 18,
    price: 1240,
    cookTime: 18,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { tea: 1, flour: 1, egg: 1, milk: 1, sugar: 1 },
  },
  {
    id: "gateau-chocolat",
    name: "ガトーショコラ",
    genre: "dessert",
    phase: "late",
    recommendedReputation: 55,
    price: 1680,
    cookTime: 21,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { chocolate: 1, flour: 1, egg: 1, milk: 1, sugar: 1 },
  },
  {
    id: "choco-banana-crepe",
    name: "チョコバナナクレープ",
    genre: "dessert",
    phase: "late",
    recommendedReputation: 55,
    price: 1720,
    cookTime: 21,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { flour: 1, egg: 1, milk: 1, chocolate: 1, fruit: 1 },
  },
  {
    id: "choco-banana-ice-crepe",
    name: "チョコバナナアイスクレープ",
    genre: "dessert",
    phase: "late",
    recommendedReputation: 70,
    price: 1960,
    cookTime: 24,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { flour: 1, egg: 1, milk: 1, chocolate: 1, fruit: 1, iceCream: 1 },
  },
  {
    id: "fruit-ice-parfait",
    name: "フルーツアイスパフェ",
    genre: "dessert",
    phase: "late",
    recommendedReputation: 70,
    price: 1660,
    cookTime: 18,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { milk: 1, fruit: 1, sugar: 1, iceCream: 1 },
  },
  {
    id: "pudding-a-la-mode",
    name: "プリンアラモード",
    genre: "dessert",
    phase: "late",
    recommendedReputation: 70,
    price: 1720,
    cookTime: 19,
    icon: ASSETS.foods.pancake,
    unlocked: false,
    isPublished: false,
    popularity: 5,
    ingredients: { egg: 1, milk: 1, sugar: 1, iceCream: 1, fruit: 1 },
  },
];

MENUS.forEach((menu) => {
  menu.icon = getMenuIcon(menu.id);
});

const INGREDIENTS = [
  { id: "milk", name: "牛乳", price: 90, initial: 5, unlockReputation: 0 },
  { id: "tea", name: "紅茶", price: 90, initial: 4, unlockReputation: 0 },
  { id: "coffee", name: "コーヒー", price: 85, initial: 4, unlockReputation: 0 },
  { id: "sugar", name: "砂糖", price: 50, initial: 5, unlockReputation: 0 },
  { id: "bread", name: "パン", price: 110, initial: 0, unlockReputation: 15 },
  { id: "rice", name: "ライス", price: 70, initial: 5, unlockReputation: 0 },
  { id: "pasta", name: "パスタ", price: 105, initial: 0, unlockReputation: 45 },
  { id: "egg", name: "卵", price: 80, initial: 6, unlockReputation: 0 },
  { id: "meat", name: "肉", price: 165, initial: 0, unlockReputation: 40 },
  { id: "flour", name: "小麦粉", price: 70, initial: 0, unlockReputation: 10 },
  { id: "vegetable", name: "野菜", price: 80, initial: 0, unlockReputation: 20 },
  { id: "fruit", name: "フルーツ", price: 150, initial: 0, unlockReputation: 30 },
  { id: "chocolate", name: "チョコ", price: 135, initial: 0, unlockReputation: 55 },
  { id: "iceCream", name: "アイス", price: 175, initial: 0, unlockReputation: 70 },
];

function getIngredientIcon(ingredientId) {
  return ASSETS.ingredients[ingredientId] || ASSETS.ingredients.unknown;
}

function getMenuIcon(menuId) {
  return ASSETS.foods[menuId] || ASSETS.foods.unknown;
}

// ここでADV立ち絵の表情を、各キャラの性格に合う基本表情へ整えます。
function normalizeAdvPortraitExpression(maidId, expression = "default") {
  if (maidId === "maid-1") {
    if (expression === "default") {
      return "serious";
    }
    if (expression === "smile") {
      return "soft_smile";
    }
  }

  if (maidId === "maid-2") {
    if (expression === "default") {
      return "soft_smile";
    }
  }

  if (maidId === "maid-3") {
    if (expression === "default") {
      return "shy";
    }
    if (expression === "smile") {
      return "soft_smile";
    }
    if (expression === "serious") {
      return "worried";
    }
  }

  return expression;
}

function normalizeAdvPortraitEntry(portrait) {
  if (!portrait?.maidId) {
    return portrait;
  }
  return {
    ...portrait,
    expression: normalizeAdvPortraitExpression(portrait.maidId, portrait.expression || "default"),
  };
}

function getAdvPortraitAsset(maidId, expression = "default") {
  const normalizedExpression = normalizeAdvPortraitExpression(maidId, expression);
  return ASSETS.adv.portraits?.[maidId]?.[normalizedExpression] || ASSETS.adv.portraits?.[maidId]?.default || ASSETS.maids[maidId] || "";
}

function getAdvBackgroundAsset(backgroundId) {
  return ASSETS.adv.backgrounds?.[backgroundId] || ASSETS.adv.backgrounds?.black || "assets/adv/backgrounds/black.png";
}

function getAdvEventCgAsset(eventCgId) {
  return ASSETS.adv.eventCg?.[eventCgId] || "";
}

function getAdvPortraitEntries(line) {
    if (Array.isArray(line?.portraits)) {
      return line.portraits.slice(0, 3).map(normalizeAdvPortraitEntry);
    }
    if (line?.speakerId && getMaidById(line.speakerId)) {
      return [normalizeAdvPortraitEntry({ maidId: line.speakerId, expression: line.expression || "default", focus: true })];
    }
    return [];
  }

function renderAdvPortraitSlots(line) {
  const slots = [
    // ここで ADV の左・中央・右の立ち絵スロットを取得します。
    { container: DOM.advPortraitLeft?.parentElement, image: DOM.advPortraitLeft },
    { container: DOM.advPortraitCenter?.parentElement, image: DOM.advPortraitCenter },
    { container: DOM.advPortraitRight?.parentElement, image: DOM.advPortraitRight },
  ];
  let portraits = getAdvPortraitEntries(line);
  // 主人公のセリフやモノローグでは直前の立ち絵を維持します。
  // ただし背景が切り替わる行は別シーン扱いにして、前の背景の立ち絵を引き継がないようにします。
  if (portraits.length === 0 && !Array.isArray(line?.portraits) && !line?.background) {
    const scene = getAdvSceneById(state.activeAdvSceneId);
    if (isAdvInlineResponseActive()) {
      const inlineIndex = state.activeAdvInlineLines.indexOf(line);
      if (inlineIndex >= 0) {
        for (let index = inlineIndex - 1; index >= 0; index -= 1) {
          const previousPortraits = getAdvPortraitEntries(state.activeAdvInlineLines[index]);
          if (previousPortraits.length > 0) {
            portraits = previousPortraits;
            break;
          }
        }
      }
      if (portraits.length === 0) {
        for (let index = state.activeAdvLineIndex; index >= 0; index -= 1) {
          const previousLine = scene?.lines?.[index];
          const previousPortraits = getAdvPortraitEntries(previousLine);
          if (previousPortraits.length > 0) {
            portraits = previousPortraits;
            break;
          }
          if (previousLine?.background) {
            break;
          }
        }
      }
    }
    const lineIndex = portraits.length === 0 ? scene?.lines?.indexOf(line) ?? -1 : -1;
    if (lineIndex >= 0) {
      for (let index = lineIndex - 1; index >= 0; index -= 1) {
        const previousLine = scene.lines[index];
        const previousPortraits = getAdvPortraitEntries(previousLine);
        if (previousPortraits.length > 0) {
          portraits = previousPortraits;
          break;
        }
        if (previousLine?.background) {
          break;
        }
      }
    }
  }
  const layoutClass = portraits.length >= 3 ? "layout-triple" : portraits.length === 2 ? "layout-double" : "layout-single";
  // ここで各立ち絵を、左・中央・右のどの表示位置へ置くかを設定します。
  const slotPortraits = [null, null, null];

  // 表示人数に合わせて、左・中央・右のどこに立ち絵を置くかを設定します。
  if (portraits.length === 1) {
    // ここで 1 人表示時は中央に立ち絵を配置します。
    slotPortraits[1] = portraits[0];
  } else if (portraits.length === 2) {
    // ここで 2 人表示時は左と右に立ち絵を配置します。
    slotPortraits[0] = portraits[0];
    slotPortraits[2] = portraits[1];
  } else {
    // ここで 3 人表示時は左・中央・右に立ち絵を配置します。
    portraits.slice(0, 3).forEach((portrait, index) => {
      slotPortraits[index] = portrait;
    });
  }

  if (DOM.advPortraitLayer) {
    DOM.advPortraitLayer.classList.remove("layout-single", "layout-double", "layout-triple");
    DOM.advPortraitLayer.classList.add(layoutClass);
  }

  slots.forEach((slot, index) => {
    const portrait = slotPortraits[index];
    if (!slot.container || !slot.image) {
      return;
    }
    slot.container.classList.remove("is-visible", "is-dim");
    if (!portrait) {
      slot.image.removeAttribute("src");
      slot.image.alt = "";
      return;
    }

    const maid = getMaidById(portrait.maidId);
    const fallbackSrc = maid ? ASSETS.maids[maid.id] : "";
    slot.image.onerror = () => {
      slot.image.onerror = null;
      if (fallbackSrc) {
        slot.image.src = fallbackSrc;
      }
    };
    slot.image.src = getAdvPortraitAsset(portrait.maidId, portrait.expression || "default");
    slot.image.alt = maid ? maidLabel(maid) : "adv portrait";
    slot.container.classList.add("is-visible");
    if (portrait.focus === false) {
      slot.container.classList.add("is-dim");
    }
  });
}

function getAdvLineEventCgId(scene, line) {
  if (!scene || !Array.isArray(scene.lines) || !line) {
    return line?.eventCg || line?.cg || "";
  }

  const lineIndex = scene.lines.indexOf(line);
  if (lineIndex < 0) {
    return line.eventCg || line.cg || "";
  }

  for (let index = lineIndex; index >= 0; index -= 1) {
    const targetLine = scene.lines[index];
    const hasEventCg = Object.prototype.hasOwnProperty.call(targetLine, "eventCg");
    const hasLegacyCg = Object.prototype.hasOwnProperty.call(targetLine, "cg");
    if (index === lineIndex && targetLine.background && !hasEventCg && !hasLegacyCg) {
      return "";
    }
    if (hasEventCg) {
      return targetLine.eventCg || "";
    }
    if (hasLegacyCg) {
      return targetLine.cg || "";
    }
    if (index !== lineIndex && targetLine.background) {
      break;
    }
  }

  return "";
}

function renderAdvEventCg(scene, line) {
  const eventCgId = getAdvLineEventCgId(scene, line);
  const eventCgAsset = eventCgId ? getAdvEventCgAsset(eventCgId) : "";
  const hasEventCg = Boolean(eventCgAsset);
  const shouldPlayIntro = hasEventCg && eventCgId !== state.activeAdvEventCgId;

  if (DOM.advStage) {
    DOM.advStage.classList.toggle("has-event-cg", hasEventCg);
  }
  if (!DOM.advEventCgLayer || !DOM.advEventCg) {
    return;
  }

  DOM.advEventCgLayer.classList.toggle("is-visible", hasEventCg);
  if (hasEventCg) {
    DOM.advEventCg.src = eventCgAsset;
    DOM.advEventCg.alt = `event cg ${eventCgId}`;
  } else {
    DOM.advEventCg.removeAttribute("src");
    DOM.advEventCg.alt = "";
  }
  state.activeAdvEventCgId = hasEventCg ? eventCgId : "";
  if (shouldPlayIntro) {
    startAdvCgIntro();
  } else if (!hasEventCg) {
    clearAdvCgIntroTimer({
      preserveBlackTransition:
        state.isAdvTransitioning &&
        DOM.advStage?.classList.contains("is-black-transition"),
    });
  }
}

function willAdvLineStartNewEventCg(scene, line) {
  const eventCgId = getAdvLineEventCgId(scene, line);
  return Boolean(eventCgId && getAdvEventCgAsset(eventCgId) && eventCgId !== state.activeAdvEventCgId);
}

function prepareAdvCgPrelude(scene, line) {
  if (!willAdvLineStartNewEventCg(scene, line)) {
    return false;
  }
  clearAdvAutoTimer();
  DOM.advStage?.classList.add("is-cg-prelude");
  return true;
}

function applyAfterAdvCgPrelude(scene, line, callback) {
  if (!prepareAdvCgPrelude(scene, line)) {
    callback();
    return;
  }
  clearAdvTransitionTimer();
  state.isAdvTransitioning = true;
  DOM.advStage?.classList.remove("is-black-transition");
  DOM.advStage?.classList.add("is-faded");
  state.advTransitionTimer = setTimeout(() => {
    callback();
    requestAnimationFrame(() => {
      DOM.advStage?.classList.remove("is-faded");
      state.advTransitionTimer = setTimeout(() => {
        state.isAdvTransitioning = false;
        state.advTransitionTimer = null;
      }, 240);
    });
  }, 320);
}

const FIXED_COSTS = {
  dailyRent: 10000,
  hourlyWage: 1200,
  maidCount: 3,
};

const FACILITY_DEFINITIONS = [
  {
    id: "kitchen",
    name: "キッチン強化",
    baseCost: 3200,
    description: "調理速度が上がり、複数注文に強くなります。",
  },
  {
    id: "seating",
    name: "ホール整備",
    baseCost: 2600,
    description: "行列受け入れと店内回転が安定します。",
  },
  {
    id: "ambiance",
    name: "内装アップ",
    baseCost: 2800,
    description: "評判と追加注文の発生率を底上げします。",
  },
];

const PROMOTION_DEFINITIONS = [
  {
    id: "sns-post",
    name: "SNS投稿",
    cost: 0,
    durationDays: 1,
    description: "費用をかけずに宣伝できます。効果は控えめですが、新メニュー直後は伸びやすくなります。",
    effectSummary: "来客率が少し上昇。新メニュー直後は効果アップ。評判50以上では炎上リスクあり。",
    tags: ["無料", "低コスト", "バズ/炎上"],
  },
  {
    id: "flyer-handout",
    name: "チラシ配り",
    cost: 2500,
    durationDays: 1,
    description: "駅前などでチラシを配って、すぐに客足を増やします。",
    effectSummary: "全体来客率がしっかり上昇。学生と会社員が目に見えて増えます。",
    tags: ["即効", "安定", "短期"],
  },
  {
    id: "store-poster",
    name: "店頭ポスター",
    cost: 6500,
    durationDays: 10,
    description: "派手さはありませんが、長くじわじわ効く店頭告知です。",
    effectSummary: "翌日から数日間、安定して来客率を大きめに押し上げます。即効性は低めですが持続力があります。",
    tags: ["長期", "安定", "高コスト"],
  },
  {
    id: "student-discount",
    name: "学割キャンペーン",
    cost: 1800,
    durationDays: 2,
    description: "学生向けの訴求で放課後の客足を集めます。",
    effectSummary: "全体の来客率が上がり、学生がしっかり増えてデザート注文も伸びます。客単価はやや低下します。",
    tags: ["学生", "デザート", "客単価低下"],
  },
  {
    id: "after-work-set",
    name: "仕事帰りセット告知",
    cost: 2200,
    durationDays: 2,
    description: "夕方の会社員に向けた訴求で、食事需要を狙います。",
    effectSummary: "18時以降の会社員来店率がかなり上がり、食事系の注文も増えます。",
    tags: ["会社員", "夕方", "食事強化"],
  },
  {
    id: "sweets-feature",
    name: "スイーツ特集",
    cost: 2600,
    durationDays: 3,
    description: "映える甘味を前面に出して、カップルや追加注文を狙います。",
    effectSummary: "カップル来店率とデザート注文率がしっかり上がり、追加注文も発生しやすくなります。",
    tags: ["カップル", "デザート", "追加注文"],
  },
];

const ADV_SCENARIOS = window.MADE_CAFE_SCENARIOS || {};
const ADV_SCENES = [
  ...(ADV_SCENARIOS.main || []),
  ...(ADV_SCENARIOS.kotone || []),
  ...(ADV_SCENARIOS.suzu || []),
  ...(ADV_SCENARIOS.hina || []),
];

const CUSTOMER_TYPES = [
  {
    id: "student",
    name: "学生",
    orderBias: {
      tea: 1,
      "milk-tea": 1,
      "fruit-tea": 2,
      "fruit-milk": 2,
      "hot-milk": 1,
      "egg-sand": 1,
      sandwich: 2,
      "french-toast": 2,
      pancake: 2,
      pudding: 1,
      "choco-pudding": 1,
      "special-pancake": 2,
      "fruit-ice-parfait": 2,
      "pudding-a-la-mode": 2,
    },
    serviceTolerance: 1.08,
    eatingSpeed: 0.8,
    stayDuration: 0.92,
    leaveIfNoPreferenceChance: 0.48,
    reputationMod: 0,
  },
  {
    id: "office-worker",
    name: "会社員",
    orderBias: {
      tea: 1,
      "black-coffee": 2,
      "cafe-au-lait": 2,
      "hot-milk": 1,
      "egg-sand": 2,
      sandwich: 2,
      hamburger: 2,
      omurice: 2,
      "fried-rice": 2,
      "tomato-pasta": 2,
      carbonara: 2,
      "hamburg-set": 2,
      oyakodon: 2,
      pudding: 1,
      "chiffon-cake": 1,
    },
    serviceTolerance: 0.88,
    eatingSpeed: 0.64,
    stayDuration: 0.58,
    leaveIfNoPreferenceChance: 0.62,
    reputationMod: -1,
  },
  {
    id: "otaku",
    name: "オタク",
    orderBias: {
      "milk-tea": 2,
      "hot-choco-milk": 2,
      "cafe-au-lait": 1,
      omurice: 2,
      "egg-sand": 1,
      hamburger: 1,
      oyakodon: 1,
      "hamburg-set": 1,
      pancake: 2,
      pudding: 1,
      "choco-pudding": 2,
      "chiffon-cake": 1,
      "gateau-chocolat": 2,
      "choco-banana-crepe": 2,
      "choco-ice-pancake": 2,
      "choco-banana-ice-crepe": 2,
      "pudding-a-la-mode": 2,
    },
    serviceTolerance: 1.2,
    eatingSpeed: 0.98,
    stayDuration: 1.02,
    leaveIfNoPreferenceChance: 0.32,
    reputationMod: 1,
  },
  {
    id: "couple",
    name: "カップル",
    orderBias: {
      "fruit-tea": 2,
      "fruit-milk": 2,
      "hot-choco-milk": 1,
      hamburger: 1,
      "tomato-pasta": 1,
      carbonara: 1,
      "hamburg-set": 1,
      "french-toast": 2,
      pancake: 1,
      "special-pancake": 2,
      "choco-pudding": 1,
      "gateau-chocolat": 2,
      "choco-banana-crepe": 2,
      "choco-ice-pancake": 1,
      "choco-banana-ice-crepe": 2,
      "fruit-ice-parfait": 2,
      "pudding-a-la-mode": 2,
    },
    serviceTolerance: 1,
    eatingSpeed: 0.9,
    stayDuration: 1.08,
    leaveIfNoPreferenceChance: 0.56,
    reputationMod: 1,
  },
];

const RECIPE_DEVELOPMENT = [
  {
    ingredients: ["tea", "sugar", "milk"],
    menuId: "milk-tea",
  },
  {
    ingredients: ["bread", "egg"],
    menuId: "egg-sand",
  },
  {
    ingredients: ["bread", "egg", "vegetable"],
    menuId: "sandwich",
  },
  {
    ingredients: ["bread", "meat", "vegetable"],
    menuId: "hamburger",
  },
  {
    ingredients: ["bread", "milk", "sugar", "egg"],
    menuId: "french-toast",
  },
  {
    ingredients: ["rice", "egg", "meat"],
    menuId: "oyakodon",
  },
  {
    ingredients: ["coffee", "milk", "sugar"],
    menuId: "cafe-au-lait",
  },
  {
    ingredients: ["tea", "sugar", "fruit"],
    menuId: "fruit-tea",
  },
  {
    ingredients: ["milk", "chocolate", "sugar"],
    menuId: "hot-choco-milk",
  },
  {
    ingredients: ["milk", "fruit", "sugar"],
    menuId: "fruit-milk",
  },
  {
    ingredients: ["rice", "egg"],
    menuId: "omurice",
  },
  {
    ingredients: ["flour", "egg", "milk", "sugar"],
    menuId: "pancake",
  },
  {
    ingredients: ["egg", "milk", "sugar"],
    menuId: "pudding",
  },
  {
    ingredients: ["rice", "meat", "vegetable"],
    menuId: "fried-rice",
  },
  {
    ingredients: ["pasta", "vegetable", "meat"],
    menuId: "tomato-pasta",
  },
  {
    ingredients: ["pasta", "egg", "milk"],
    menuId: "carbonara",
  },
  {
    ingredients: ["egg", "milk", "sugar", "chocolate"],
    menuId: "choco-pudding",
  },
  {
    ingredients: ["rice", "meat", "egg", "vegetable"],
    menuId: "hamburg-set",
  },
  {
    ingredients: ["tea", "flour", "egg", "milk", "sugar"],
    menuId: "chiffon-cake",
  },
  {
    ingredients: ["chocolate", "flour", "egg", "milk", "sugar"],
    menuId: "gateau-chocolat",
  },
  {
    ingredients: ["flour", "egg", "milk", "sugar", "fruit"],
    menuId: "special-pancake",
  },
  {
    ingredients: ["flour", "egg", "milk", "chocolate", "fruit"],
    menuId: "choco-banana-crepe",
  },
  {
    ingredients: ["flour", "egg", "milk", "sugar", "chocolate", "iceCream"],
    menuId: "choco-ice-pancake",
  },
  {
    ingredients: ["flour", "egg", "milk", "chocolate", "fruit", "iceCream"],
    menuId: "choco-banana-ice-crepe",
  },
  {
    ingredients: ["milk", "fruit", "sugar", "iceCream"],
    menuId: "fruit-ice-parfait",
  },
  {
    ingredients: ["egg", "milk", "sugar", "iceCream", "fruit"],
    menuId: "pudding-a-la-mode",
  },
];

const CUSTOMER_AVATARS = ["(^_^)", "(o_o)", "(^o^)", "('v')", "(^.^)"];

const TASK_DEFINITIONS = {
  takeOrder: { duration: 4, bubble: TEXT.takeOrderBubble, cycle: "order" },
  cookMeal: { duration: 0, bubble: TEXT.cookBubble, cycle: "cook" },
  serveMeal: { duration: 4, bubble: TEXT.serveBubble, cycle: "serve" },
  cleanup: { duration: 12, bubble: TEXT.cleanBubble, cycle: "clean" },
};

// ここで料理の縮小率設定を行います。
// テーブル上に表示する料理素材だけ、この倍率で縮小表示します。
// 実際の素材サイズと、皿の大きさ・背の高さを見ながら料理IDごとに個別設定できるようにしています。
const TABLE_MEAL_SCALES = {
  "tea": 0.8,
  "black-coffee": 0.6,
  "hot-milk": 0.6,
  "milk-tea": 0.07,
  "cafe-au-lait": 0.06,
  "fruit-tea": 0.1,
  "hot-choco-milk": 0.09,
  "fruit-milk": 0.1,
  "egg-sand": 0.11,
  "sandwich": 0.1,
  "hamburger": 0.11,
  "french-toast": 0.4,
  "oyakodon": 0.1,
  "omurice": 0.08,
  "fried-rice": 0.1,
  "tomato-pasta": 0.1,
  "carbonara": 0.08,
  "hamburg-set": 0.1,
  "pancake": 0.1,
  "pudding": 0.11,
  "choco-pudding": 0.1,
  "special-pancake": 0.1,
  "chiffon-cake": 0.08,
  "gateau-chocolat": 0.12,
  "choco-banana-crepe": 0.11,
  "choco-ice-pancake": 0.1,
  "choco-banana-ice-crepe": 0.1,
  "fruit-ice-parfait": 0.1,
  "pudding-a-la-mode": 0.1,
};

const DOM = {
  titleScreen: document.getElementById("title-screen"),
  titleLogo: document.getElementById("title-logo"),
  titleNewGameBtn: document.getElementById("title-new-game-btn"),
  titleContinueBtn: document.getElementById("title-continue-btn"),
  endingThanksScreen: document.getElementById("ending-thanks-screen"),
  endingThanksCg: document.getElementById("ending-thanks-cg"),
  endingThanksLogo: document.getElementById("ending-thanks-logo"),
  screenFadeOverlay: document.getElementById("screen-fade-overlay"),
  viewport: document.getElementById("viewport"),
  floor: document.getElementById("floor"),
  hallTitle: document.getElementById("hall-title"),
  clock: document.getElementById("clock"),
  money: document.getElementById("money"),
  reputation: document.getElementById("reputation"),
  sales: document.getElementById("sales"),
  speedPauseBtn: document.getElementById("speed-pause-btn"),
  speedNormalBtn: document.getElementById("speed-normal-btn"),
  speedFastBtn: document.getElementById("speed-fast-btn"),
  shopStatus: document.getElementById("shop-status"),
  kitchenStatus: document.getElementById("kitchen-status"),
  dishPreview: document.getElementById("dish-preview"),
  maidsLayer: document.getElementById("maids-layer"),
  tables: document.getElementById("tables"),
  log: document.getElementById("log"),
  cycleList: document.getElementById("cycle-list"),
  waitingLineIndicator: document.getElementById("waiting-line-indicator"),
  ingredientGrid: document.getElementById("ingredient-grid"),
  selectedIngredients: document.getElementById("selected-ingredients"),
  developBtn: document.getElementById("develop-btn"),
  recipeResult: document.getElementById("recipe-result"),
  recipeResultVisual: document.getElementById("recipe-result-visual"),
  recipeBookList: document.getElementById("recipe-book-list"),
  recipeOpenBtn: document.getElementById("recipe-open-btn"),
  recipeCloseBtn: document.getElementById("recipe-close-btn"),
  recipeModalBackdrop: document.getElementById("recipe-modal-backdrop"),
  recipeBookOpenBtn: document.getElementById("recipe-book-open-btn"),
  recipeBookCloseBtn: document.getElementById("recipe-book-close-btn"),
  recipeBookModalBackdrop: document.getElementById("recipe-book-modal-backdrop"),
  advOpenBtn: document.getElementById("adv-open-btn"),
  advCloseBtn: document.getElementById("adv-close-btn"),
  advModalBackdrop: document.getElementById("adv-modal-backdrop"),
  advModal: document.getElementById("adv-modal"),
  advSceneList: document.getElementById("adv-scene-list"),
  advStageShell: document.getElementById("adv-stage-shell"),
  advStage: document.getElementById("adv-stage"),
  advBackground: document.getElementById("adv-background"),
  advEventCgLayer: document.getElementById("adv-event-cg-layer"),
  advEventCg: document.getElementById("adv-event-cg"),
  advPortraitLayer: document.querySelector(".adv-portrait-layer"),
  advPortraitLeft: document.getElementById("adv-portrait-left"),
  advPortraitCenter: document.getElementById("adv-portrait-center"),
  advPortraitRight: document.getElementById("adv-portrait-right"),
  advSceneTitle: document.getElementById("adv-scene-title"),
  advTextbox: document.getElementById("adv-textbox"),
  advSpeaker: document.getElementById("adv-speaker"),
  advText: document.getElementById("adv-text"),
  advChoices: document.getElementById("adv-choices"),
  advContinue: document.getElementById("adv-continue"),
  advNextBtn: document.getElementById("adv-next-btn"),
  advAutoBtn: document.getElementById("adv-auto-btn"),
  advSkipBtn: document.getElementById("adv-skip-btn"),
  inventoryOpenBtn: document.getElementById("inventory-open-btn"),
  inventoryCloseBtn: document.getElementById("inventory-close-btn"),
  inventoryModalBackdrop: document.getElementById("inventory-modal-backdrop"),
  inventoryList: document.getElementById("inventory-list"),
  inventoryDeliveryStatus: document.getElementById("inventory-delivery-status"),
  inventoryCartSummary: document.getElementById("inventory-cart-summary"),
  inventoryCartClearBtn: document.getElementById("inventory-cart-clear-btn"),
  inventoryOrderBtn: document.getElementById("inventory-order-btn"),
  maidSelector: document.getElementById("maid-selector"),
  selectedMaid: document.getElementById("selected-maid"),
  maidListOpenBtn: document.getElementById("maid-list-open-btn"),
  maidListCloseBtn: document.getElementById("maid-list-close-btn"),
  maidListModalBackdrop: document.getElementById("maid-list-modal-backdrop"),
  maidList: document.getElementById("maid-list"),
  facilityOpenBtn: document.getElementById("facility-open-btn"),
  facilityCloseBtn: document.getElementById("facility-close-btn"),
  facilityModalBackdrop: document.getElementById("facility-modal-backdrop"),
  facilityList: document.getElementById("facility-list"),
  promotionOpenBtn: document.getElementById("promotion-open-btn"),
  promotionCloseBtn: document.getElementById("promotion-close-btn"),
  promotionModalBackdrop: document.getElementById("promotion-modal-backdrop"),
  promotionStatus: document.getElementById("promotion-status"),
  promotionList: document.getElementById("promotion-list"),
  slgSaveBtn: document.getElementById("slg-save-btn"),
  slgLoadBtn: document.getElementById("slg-load-btn"),
  slgDeleteSaveBtn: document.getElementById("slg-delete-save-btn"),
  dailyReportModalBackdrop: document.getElementById("daily-report-modal-backdrop"),
  dailyReportCloseBtn: document.getElementById("daily-report-close-btn"),
  dailyReportSkipCheckbox: document.getElementById("daily-report-skip-checkbox"),
  dailyReportTitle: document.getElementById("daily-report-title"),
  dailyReportSales: document.getElementById("daily-report-sales"),
  dailyReportGuests: document.getElementById("daily-report-guests"),
  dailyReportReputation: document.getElementById("daily-report-reputation"),
  dailyReportMoney: document.getElementById("daily-report-money"),
  dailyReportFixedCosts: document.getElementById("daily-report-fixed-costs"),
  dailyReportProfit: document.getElementById("daily-report-profit"),
  dailyReportWalkouts: document.getElementById("daily-report-walkouts"),
  dailyReportSatisfaction: document.getElementById("daily-report-satisfaction"),
  dailyReportRanking: document.getElementById("daily-report-ranking"),
  dailyReportSummary: document.getElementById("daily-report-summary"),
  deliveryReportModalBackdrop: document.getElementById("delivery-report-modal-backdrop"),
  deliveryReportModal: document.getElementById("delivery-report-modal"),
  deliveryReportCloseBtn: document.getElementById("delivery-report-close-btn"),
  deliveryReportSkipCheckbox: document.getElementById("delivery-report-skip-checkbox"),
  deliveryReportTitle: document.getElementById("delivery-report-title"),
  deliveryReportItems: document.getElementById("delivery-report-items"),
  deliveryReportSummary: document.getElementById("delivery-report-summary"),
  deliveryBoxItemA: document.getElementById("delivery-box-item-a"),
  deliveryBoxItemB: document.getElementById("delivery-box-item-b"),
  deliveryBoxItemC: document.getElementById("delivery-box-item-c"),
  systemNoticeModalBackdrop: document.getElementById("system-notice-modal-backdrop"),
  systemNoticeTitle: document.getElementById("system-notice-title"),
  systemNoticeBody: document.getElementById("system-notice-body"),
  systemNoticeOkBtn: document.getElementById("system-notice-ok-btn"),
};

const state = {
  day: 1,
  currentMinute: START_MINUTE,
  money: 1000000,
  reputation: 0,
  reputationValue: 0,
  dailySales: 0,
  dailyMenuOrders: {},
  dailyGuests: 0,
  dailyFixedCosts: getTotalFixedCosts(),
  dailyWalkouts: 0,
  dailySatisfactionTotal: 0,
  dailyFinishedGroups: 0,
  guestCount: 0,
  nextGuestAt: OPEN_MINUTE + 8,
  cycleStep: null,
  activeKitchenOrder: null,
  closingLogged: false,
  daySettled: false,
  nextDayQueued: false,
    waitingCustomers: [],
    // ここで各席の基準座標と、客・料理・食器の表示位置を設定します。
    tables: [
    // ここで 上段左席の座標を設定します。
    makeTable("A-1", {
      x: 270,
      y: 154,
      serviceOffsetX: 10,
      serviceOffsetY: -12,
      customerX: 32,
      customerY: -48,
      mealX: 32,
      mealY: 20,
      dishX: 32,
      dishY: 24,
      unlockLevel: 1,
    }),
    // ここで 上段右席の座標を設定します。
    makeTable("A-2", {
      x: 442,
      y: 154,
      serviceOffsetX: 10,
      serviceOffsetY: -12,
      customerX: 32,
      customerY: -44,
      mealX: 32,
      mealY: 20,
      dishX: 32,
      dishY: 24,
      unlockLevel: 2,
    }),
    // ここで 下段左席の座標を設定します。
    makeTable("A-3", {
      x: 180,
      y: 298,
      serviceOffsetX: 10,
      serviceOffsetY: -12,
      customerX: 32,
      customerY: -48,
      mealX: 32,
      mealY: 20,
      dishX: 32,
      dishY: 24,
      unlockLevel: 0,
    }),
    // ここで 下段中央席の座標を設定します。
    makeTable("B-2", {
      x: 356,
      y: 298,
      serviceOffsetX: 10,
      serviceOffsetY: -12,
      customerX: 32,
      customerY: -44,
      mealX: 32,
      mealY: 20,
      dishX: 32,
      dishY: 24,
      unlockLevel: 0,
    }),
    // ここで 下段右席の座標を設定します。
    makeTable("B-3", {
      x: 532,
      y: 298,
      serviceOffsetX: 10,
      serviceOffsetY: -12,
      customerX: 32,
      customerY: -48,
      mealX: 32,
      mealY: 20,
      dishX: 32,
      dishY: 24,
      unlockLevel: 0,
    }),
  ],
  taskQueue: [],
  // ここでメイドの初期位置を設定します。
  maids: [
    // ここで ことね の初期位置を設定します。
    makeMaid(
      "maid-1",
      "ことね",
      100,
      210,
      "",
      "^_^",
      { service: 5, cooking: 3, research: 2 },
      { takeOrder: 3, serveMeal: 2, cleanup: -1, flavorText: "注文を積極的に取りにいく接客タイプです。" }
    ),
    // ここで すず の初期位置を設定します。
    makeMaid(
      "maid-2",
      "すず",
      60,
      260,
      "variant-mint",
      "o_o",
      { service: 3, cooking: 5, research: 4 },
      { cookMeal: 3, cleanup: 1, takeOrder: -1, flavorText: "キッチンに入ると強く、調理を取りやすいタイプです。" }
    ),
    // ここで ひな の初期位置を設定します。
    makeMaid(
      "maid-3",
      "ひな",
      10,
      200,
      "variant-gold",
      "*_*",
      { service: 4, cooking: 4, research: 5 },
      { cleanup: 3, serveMeal: 1, takeOrder: 1, flavorText: "片付けやフォローが得意で、空いた仕事を埋めるのが上手です。" }
    ),
  ],
  logEntries: [],
  selectedIngredients: [],
  selectedResearchMaidId: "maid-1",
  timeScale: 1,
  recipeResult: "食材を選ぶとここに結果が出ます。",
  recipeResultImage: "",
  isTitleScreenOpen: true,
  isEndingThanksScreenOpen: false,
  isRecipeModalOpen: false,
  isRecipeBookOpen: false,
  isAdvModalOpen: false,
  isAdvTransitioning: false,
  isAdvDebugMode: false,
  isInventoryModalOpen: false,
  pendingInventoryOrder: {},
  hasOrderedToday: false,
  pendingInventoryDelivery: null,
  isMaidListOpen: false,
  isFacilityModalOpen: false,
  isPromotionModalOpen: false,
  isDailyReportOpen: false,
  skipDailyReport: false,
  isDeliveryReportOpen: false,
  isSystemNoticeOpen: false,
  skipDeliveryReport: false,
  pendingGameOverAfterDelivery: false,
  pendingAdvanceAfterAdv: false,
  unlockedAdvSceneIds: [],
  completedAdvSceneIds: [],
  activeAdvSceneId: null,
  activeAdvLineIndex: 0,
  activeAdvInlineLines: [],
  activeAdvInlineLineIndex: 0,
  activeAdvInlineReturnIndex: null,
  advChoiceHistory: {},
  advTransitionTimer: null,
  advAutoTimer: null,
  advCgIntroTimer: null,
  activeAdvEventCgId: "",
  isAdvCgIntro: false,
  isAdvAutoMode: false,
  advLineAutoTimer: null,
  isAdvLineAutoWaiting: false,
  dailyReportAutoCloseTimer: null,
  deliveryReportAutoCloseTimer: null,
  nightTalkHistory: {},
  nightTalkSelections: {},
  systemNoticeQueue: [],
  shownSystemNoticeIds: [],
  activeSystemNotice: null,
  dayStartMoney: 1000000,
  dayStartReputation: 0,
  lastDailyReport: null,
  lastDeliveryReport: null,
  hasPromotedToday: false,
  activePromotions: [],
  lastUnlockedMenuId: null,
  lastUnlockedMenuDay: 0,
  trialEndShown: false,
  lastSnsOutcome: null,
  promotionUseCount: 0,
  snsPostCount: 0,
  snsFlameCount: 0,
  consecutiveDeficitDays: 0,
  profitableDays: 0,
  postFlameProfitableDays: 0,
  hadSnsFlame: false,
  facilityUpgradeCount: 0,
  chapterTenKitchenApplied: false,
  chapterElevenReputationPenaltyApplied: false,
  forceNextSnsFlame: false,
  isFlamePenaltyActive: false,
  flamePenaltyDaysRemaining: 0,
  isChapterLockedFlamePenalty: false,
  ingredientMarket: Object.fromEntries(INGREDIENTS.map((ingredient) => [ingredient.id, ingredient.price])),
  unlockedIngredientIds: INGREDIENTS.filter((ingredient) => ingredient.unlockReputation <= 0).map((ingredient) => ingredient.id),
  facilities: { kitchen: 0, seating: 0, ambiance: 0 },
  systemUnlocks: {
    inventoryManagement: false,
    recipeDevelopment: false,
    promotion: false,
    menuPublication: false,
  },
  hasInventoryNotice: false,
  hasRecipeDevelopmentNotice: false,
  affection: {
    "maid-1": 0,
    "maid-2": 0,
    "maid-3": 0,
  },
  inventory: Object.fromEntries(
    INGREDIENTS.map((ingredient) => [
      ingredient.id,
      ingredient.id === "coffee" || ingredient.id === "tea" || ingredient.id === "milk" ? 20 : 0,
    ])
  ),
};

function makeTable(id, layout) {
  return {
    id,
    // ここで席そのものの配置座標を設定します。
    x: layout.x,
    y: layout.y,
    unlockLevel: layout.unlockLevel ?? 0,
    // ここでメイドが接客に立つ位置のずらし量を設定します。
    serviceOffsetX: layout.serviceOffsetX ?? 10,
    serviceOffsetY: layout.serviceOffsetY ?? -12,
    // ここで客・料理・食器を席のどこに表示するかを設定します。
    customerX: layout.customerX ?? 32,
    customerY: layout.customerY ?? -48,
    mealX: layout.mealX ?? 32,
    mealY: layout.mealY ?? 42,
    dishX: layout.dishX ?? 32,
    dishY: layout.dishY ?? 48,
    occupant: null,
    settledMenus: [],
    mealServed: false,
    dirty: false,
    waitingForSales: false,
    pendingCleanup: false,
    pendingServedMenus: [],
    salesCustomerTypeId: null,
    statusText: TEXT.empty,
    visualMeals: [],
    visualDish: false,
  };
}

function makeMaid(id, name, x, y, variantClass, face, stats, traits = {}) {
  return {
    id,
    name,
    // ここでメイドの現在位置と初期位置を設定します。
    x,
    y,
    homeX: x,
    homeY: y,
    variantClass,
    face,
    task: null,
    bubbleText: TEXT.idle,
    stats,
    traits: {
      takeOrder: traits.takeOrder ?? 0,
      cookMeal: traits.cookMeal ?? 0,
      serveMeal: traits.serveMeal ?? 0,
      cleanup: traits.cleanup ?? 0,
      flavorText: traits.flavorText || "",
    },
    exp: {
      service: 0,
      cooking: 0,
      research: 0,
    },
  };
}

function makeCustomer(tableId) {
  const avatar = CUSTOMER_AVATARS[state.guestCount % CUSTOMER_AVATARS.length];
  const customerType = chooseCustomerType();
  const avatarImage = ASSETS.customers[customerType?.id] || ASSETS.customers.student;
  const orders = chooseMenusForCustomer(customerType);
  const primaryMenu = orders[0] || null;
  return {
    id: `guest-${state.guestCount + 1}`,
    avatar,
    avatarImage,
    tableId,
    customerType,
    orders,
    orderName: orders.map((menu) => menu.name).join(" / "),
    currentOrderIndex: 0,
    state: "arriving",
    arrivedAt: state.currentMinute,
    queuedAt: null,
    orderedAt: null,
    servedAt: null,
    eatUntil: null,
    leaveAt: null,
    menu: primaryMenu,
    mood: 0,
    satisfaction: 0,
    compromiseCount: 0,
    waitingLimit: Math.max(12, Math.round(18 * (customerType?.serviceTolerance || 1))),
  };
}

function resizeGame() {
  const scale = Math.min(window.innerWidth / BASE_WIDTH, window.innerHeight / BASE_HEIGHT);
  const offsetX = (window.innerWidth - BASE_WIDTH * scale) / 2;
  const offsetY = (window.innerHeight - BASE_HEIGHT * scale) / 2;
  DOM.viewport.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
}

function formatTime(totalMinutes) {
  const hour = Math.floor(totalMinutes / 60);
  const minute = totalMinutes % 60;
  return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
}

function yen(value) {
  return `${value.toLocaleString("ja-JP")}${TEXT.yen}`;
}

function isOpen() {
  return state.currentMinute >= OPEN_MINUTE && state.currentMinute < CLOSE_MINUTE;
}

function isNightFloorTime() {
  return state.currentMinute >= 17 * 60 + 30;
}

function hasActiveCustomers() {
  return state.waitingCustomers.length > 0 || getActiveTables().some((table) => table.occupant || table.dirty || table.pendingCleanup || table.waitingForSales);
}

function getTable(id) {
  return state.tables.find((table) => table.id === id);
}

function getActiveSeatCount() {
  return Math.min(5, 3 + state.facilities.seating);
}

function getActiveTables() {
  return state.tables.filter((table) => table.unlockLevel < getActiveSeatCount() - 2);
}

function getMenuById(id) {
  return MENUS.find((menu) => menu.id === id);
}

function isInitialMenu(menuId) {
  return ["tea", "black-coffee", "hot-milk"].includes(menuId);
}

function ensureInitialMenusAvailable() {
  ["tea", "black-coffee", "hot-milk"].forEach((menuId) => {
    const menu = getMenuById(menuId);
    if (!menu) {
      return;
    }
    menu.unlocked = true;
  });
}

function areInitialDrinkMenusPublished() {
  return ["tea", "black-coffee", "hot-milk"].every((menuId) => {
    const menu = getMenuById(menuId);
    return menu?.unlocked && menu?.isPublished;
  });
}

function getIngredientById(id) {
  return INGREDIENTS.find((ingredient) => ingredient.id === id);
}

function getIngredientMarketPrice(id) {
  return state.ingredientMarket[id] || getIngredientById(id)?.price || 0;
}

function getUnlockedIngredients() {
  return INGREDIENTS.filter((ingredient) => state.unlockedIngredientIds.includes(ingredient.id));
}

function formatMenuIngredients(menu) {
  return Object.entries(menu.ingredients)
    .map(([ingredientId, required]) => {
      const ingredient = getIngredientById(ingredientId);
      return `${ingredient ? ingredient.name : ingredientId} x${required}`;
    })
    .join(" / ");
}

function renderMenuIngredientTags(menu) {
  return Object.entries(menu.ingredients)
    .map(([ingredientId, required]) => {
      const ingredient = getIngredientById(ingredientId);
      const label = ingredient ? ingredient.name : ingredientId;
      return `<span class="recipe-ingredient-tag"><img src="${getIngredientIcon(ingredientId)}" alt="${label}"><span>${label} x${required}</span></span>`;
    })
    .join("");
}

function getMenuCost(menu) {
  return Object.entries(menu.ingredients).reduce((sum, [ingredientId, required]) => {
    return sum + getIngredientMarketPrice(ingredientId) * required;
  }, 0);
}

function getMenuMarginRate(menu) {
  if (!menu.price) {
    return 0;
  }
  return Math.round(((menu.price - getMenuCost(menu)) / menu.price) * 100);
}

function getMaidById(id) {
  return state.maids.find((maid) => maid.id === id);
}

function getCustomerTypeById(id) {
  return CUSTOMER_TYPES.find((customerType) => customerType.id === id);
}

function hasPreferredPublishedMenu(customerType) {
  if (!customerType?.orderBias) {
    return true;
  }
  return getPublishedMenus().some((menu) => customerType.orderBias[menu.id] && customerType.orderBias[menu.id] > 0);
}

function shouldLeaveWithoutPreferredMenu(customerType) {
  if (areInitialDrinkMenusPublished()) {
    return false;
  }
  let baseChance = customerType?.leaveIfNoPreferenceChance ?? 0.8;
  const publishedDrinkCount = getPublishedMenus().filter((menu) => menu.genre === "drink").length;
  if (publishedDrinkCount >= 3) {
    baseChance *= 0.45;
  }
  const reputationDiscount = Math.min(0.35, state.reputation * 0.012);
  const adjustedChance = Math.max(0.08, baseChance - reputationDiscount);
  return Math.random() < adjustedChance;
}

function getCurrentCustomerOrder(customer) {
  if (!customer || !Array.isArray(customer.orders)) {
    return null;
  }
  return customer.orders[customer.currentOrderIndex] || null;
}

function getPendingCustomerOrders(customer) {
  if (!customer || !Array.isArray(customer.orders)) {
    return [];
  }
  return customer.orders.slice(customer.currentOrderIndex);
}

function getCustomerOrderSummary(customer) {
  if (!customer || !Array.isArray(customer.orders) || customer.orders.length === 0) {
    return "";
  }
  return customer.orders.map((menu) => menu.name).join(" / ");
}

function getCustomerLogName(customer) {
  const typeName = customer?.customerType?.name || "お客さん";
  return `${typeName}のお客さん`;
}

function getMealPositionClasses(mealVisuals) {
  if (!Array.isArray(mealVisuals) || mealVisuals.length === 0) {
    return [];
  }

  if (mealVisuals.length === 1) {
    return ["meal-center"];
  }

  if (mealVisuals.length === 2) {
    return ["meal-pair-left", "meal-pair-right"];
  }

  const classes = new Array(mealVisuals.length).fill("meal-right");
  const mealIndex = mealVisuals.findIndex((mealVisual) => mealVisual.genre === "meal");

  if (mealIndex >= 0) {
    classes[mealIndex] = "meal-triple-center";
    const remainingIndexes = mealVisuals.map((_, index) => index).filter((index) => index !== mealIndex);
    if (remainingIndexes[0] != null) {
      classes[remainingIndexes[0]] = "meal-left";
    }
    if (remainingIndexes[1] != null) {
      classes[remainingIndexes[1]] = "meal-right";
    }
    return classes;
  }

  return ["meal-left", "meal-triple-center", "meal-right"].slice(0, mealVisuals.length);
}

function clearRecipeResultDisplay() {
  state.recipeResult = "食材を選ぶとここに結果が出ます。";
  state.recipeResultImage = "";
}

function findCompromiseMenuForCustomer(customer, unavailableMenu, replaceIndex = customer?.currentOrderIndex ?? 0) {
  if (!customer || !unavailableMenu) {
    return null;
  }

  const genreLimit = customer.customerType?.id === "couple" ? 2 : 1;
  const genreCounts = customer.orders.reduce((counts, menu, index) => {
    if (index !== replaceIndex) {
      counts[menu.genre] = (counts[menu.genre] || 0) + 1;
    }
    return counts;
  }, {});
  const orderedMenuIds = new Set(
    customer.orders
      .filter((menu, index) => index !== replaceIndex)
      .map((menu) => menu.id)
  );

  const candidates = buildWeightedMenus(customer.customerType).filter((entry) => {
    return (
      entry.menu.id !== unavailableMenu.id &&
      hasIngredientsForMenu(entry.menu) &&
      !orderedMenuIds.has(entry.menu.id) &&
      (genreCounts[entry.menu.genre] || 0) < genreLimit
    );
  });

  if (candidates.length === 0) {
    return null;
  }

  const sameGenreCandidates = candidates.filter((entry) => entry.menu.genre === unavailableMenu.genre);
  return chooseWeightedMenu(sameGenreCandidates.length ? sameGenreCandidates : candidates);
}

function maybeAppendExtraOrder(customer) {
  if (!customer || customer.orders.length >= 6) {
    return null;
  }
  const genreCounts = customer.orders.reduce((counts, menu) => {
    counts[menu.genre] = (counts[menu.genre] || 0) + 1;
    return counts;
  }, {});
  const genreLimit = customer.customerType?.id === "couple" ? 2 : 1;
  const orderedMenuIds = new Set(customer.orders.map((menu) => menu.id));
  const candidates = buildWeightedMenus(customer.customerType).filter((entry) => (genreCounts[entry.menu.genre] || 0) < genreLimit && !orderedMenuIds.has(entry.menu.id));
  if (candidates.length === 0) {
    return null;
  }

  const extraChance =
    0.08 +
    state.facilities.ambiance * 0.05 +
    (customer.customerType?.id === "couple" ? 0.1 : customer.customerType?.id === "otaku" ? 0.08 : 0) +
    getPromotionExtraOrderBonus(customer.customerType);
  if (Math.random() > extraChance) {
    return null;
  }

  const extraMenu = chooseWeightedMenu(candidates);
  if (!extraMenu) {
    return null;
  }
  customer.orders.push(extraMenu);
  return extraMenu;
}

function formatMaidStats(maid) {
  return `接客 ${maid.stats.service} / 調理 ${maid.stats.cooking} / 開発 ${maid.stats.research}`;
}

function getWaitingCapacity() {
  return 5;
}

function getFacilityById(id) {
  return FACILITY_DEFINITIONS.find((facility) => facility.id === id);
}

function getFacilityUpgradeCost(facilityId) {
  const level = state.facilities[facilityId] || 0;
  const definition = getFacilityById(facilityId);
  return definition ? definition.baseCost + level * 1800 : 0;
}

function getPromotionById(id) {
  return PROMOTION_DEFINITIONS.find((promotion) => promotion.id === id) || null;
}

function getActivePromotion(id) {
  return state.activePromotions.find((promotion) => promotion.id === id) || null;
}

function hasRecentMenuUnlock() {
  return !!state.lastUnlockedMenuId && state.day - (state.lastUnlockedMenuDay || 0) <= 1;
}

function isPromotionEffectiveToday(promotion) {
  return state.day >= (promotion.startsOnDay || promotion.startedOnDay || 1);
}

function getPromotionVisitBonus() {
  return state.activePromotions.reduce((sum, promotion) => {
    if (!isPromotionEffectiveToday(promotion)) {
      return sum;
    }
    if (promotion.id === "sns-post") {
      return sum + 0.09 + (promotion.recentRecipeBoost ? 0.08 : 0);
    }
    if (promotion.id === "flyer-handout") {
      return sum + 0.2;
    }
    if (promotion.id === "store-poster") {
      return sum + 0.12;
    }
    if (promotion.id === "student-discount") {
      return sum + 0.12;
    }
    if (promotion.id === "after-work-set") {
      return sum + 0.09;
    }
    if (promotion.id === "sweets-feature") {
      return sum + 0.11;
    }
    if (promotion.id === "sns-viral") {
      return sum + 0.26;
    }
    return sum;
  }, 0);
}

function getFlameVisitIntervalPenalty() {
  if (!state.isFlamePenaltyActive) {
    return 0;
  }
  return state.isChapterLockedFlamePenalty ? 14 : 6;
}

function getFlameVisitChancePenalty() {
  if (!state.isFlamePenaltyActive) {
    return 0;
  }
  return state.isChapterLockedFlamePenalty ? 0.42 : 0.16;
}

function getPromotionCustomerTypeWeightBonus(customerTypeId) {
  let bonus = 0;
  state.activePromotions.forEach((promotion) => {
    if (!isPromotionEffectiveToday(promotion)) {
      return;
    }
    if (promotion.id === "flyer-handout") {
      if (customerTypeId === "student") bonus += 0.18;
      if (customerTypeId === "office-worker") bonus += 0.14;
    }
    if (promotion.id === "student-discount" && customerTypeId === "student") {
      bonus += 0.32;
    }
    if (promotion.id === "after-work-set" && customerTypeId === "office-worker" && state.currentMinute >= 18 * 60 && state.currentMinute < 20 * 60) {
      bonus += 0.35;
    }
    if (promotion.id === "sweets-feature" && customerTypeId === "couple") {
      bonus += 0.24;
    }
    if (promotion.id === "sns-viral") {
      bonus += 0.09;
    }
  });
  return bonus;
}

function getPromotionMenuWeightBonus(menu, customerType) {
  let bonus = 0;
  state.activePromotions.forEach((promotion) => {
    if (!isPromotionEffectiveToday(promotion)) {
      return;
    }
    if (promotion.id === "student-discount" && customerType?.id === "student" && menu.genre === "dessert") {
      bonus += 3;
    }
    if (promotion.id === "after-work-set" && state.currentMinute >= 18 * 60 && state.currentMinute < 20 * 60 && menu.genre === "meal") {
      bonus += 4;
    }
    if (promotion.id === "sweets-feature" && menu.genre === "dessert") {
      bonus += 4;
    }
    if (promotion.id === "sns-post" && promotion.recentRecipeBoost && menu.id === state.lastUnlockedMenuId) {
      bonus += 4;
    }
    if (promotion.id === "sns-viral") {
      if (menu.id === promotion.featuredMenuId) {
        bonus += 6;
      } else {
        bonus += 1.5;
      }
    }
  });
  return bonus;
}

function getPromotionExtraOrderBonus(customerType) {
  let bonus = 0;
  state.activePromotions.forEach((promotion) => {
    if (!isPromotionEffectiveToday(promotion)) {
      return;
    }
    if (promotion.id === "sweets-feature") {
      bonus += 0.12;
    }
    if (promotion.id === "sns-viral") {
      bonus += 0.08;
    }
    if (promotion.id === "student-discount" && customerType?.id === "student") {
      bonus += 0.06;
    }
  });
  return bonus;
}

function getPromotionSalesMultiplier(customerTypeId) {
  let multiplier = 1;
  state.activePromotions.forEach((promotion) => {
    if (!isPromotionEffectiveToday(promotion)) {
      return;
    }
    if (promotion.id === "student-discount" && customerTypeId === "student") {
      multiplier *= 0.92;
    }
  });
  return multiplier;
}

function getPromotionStatusText() {
  const activeLabels = state.activePromotions
    .filter((promotion) => promotion.id !== "sns-post")
    .map((promotion) =>
      `${promotion.name} ${isPromotionEffectiveToday(promotion) ? `残り${promotion.daysRemaining}日` : "明日から開始"}`
    );
  const todayLabel = state.hasPromotedToday ? "本日の広報は実施済みです。" : "本日の広報はまだ実施していません。";
  const snsLabel =
    state.lastSnsOutcome?.type === "viral"
      ? `直近のSNS投稿はバズりました。残り${getActivePromotion("sns-viral")?.daysRemaining || 0}日ほど効果があります。`
      : state.lastSnsOutcome?.type === "flame"
        ? "直近のSNS投稿は炎上し、評判が大きく下がりました。"
        : "";
  return [todayLabel, activeLabels.join(" / "), snsLabel].filter(Boolean).join(" ");
}

function addMaidExp(maid, statKey, amount, reason = "") {
  if (!maid?.exp || !maid.stats[statKey]) {
    return;
  }
  maid.exp[statKey] += amount;
  const threshold = maid.stats[statKey] * 3;
  if (maid.exp[statKey] >= threshold && maid.stats[statKey] < MAX_MAID_STAT) {
      maid.exp[statKey] = 0;
      maid.stats[statKey] += 1;
      pushLog(`${maidLabel(maid)}の${statKey === "service" ? "接客" : statKey === "cooking" ? "調理" : "開発"}能力が上がりました。${reason}`, "成長");
    }
  }

function getMaidExpThreshold(maid, statKey) {
  if (!maid?.stats?.[statKey]) {
    return 0;
  }
  return maid.stats[statKey] >= MAX_MAID_STAT ? 0 : maid.stats[statKey] * 3;
}

function getMaidTaskDuration(maid, taskType, table) {
  const currentOrder = getCurrentCustomerOrder(table?.occupant);
  const pendingOrders = getPendingCustomerOrders(table?.occupant);
  const baseDuration =
    taskType === "cookMeal" && currentOrder
      ? Math.max(
          currentOrder.cookTime,
          Math.round(
            pendingOrders.reduce((sum, menu) => sum + menu.cookTime, 0) *
            // ここで同時注文分をまとめて調理する時の時短係数を設定します。
            0.78
          )
        )
      : TASK_DEFINITIONS[taskType]?.duration || 0;
  const relevantStat = taskType === "cookMeal" ? maid.stats.cooking : maid.stats.service;
  const facilityBoost = taskType === "cookMeal" ? state.facilities.kitchen * 0.05 : state.facilities.seating * 0.02;
  const speedMultiplier = Math.max(0.5, 1.12 - relevantStat * 0.08 - facilityBoost);
  return Math.max(8, Math.round(baseDuration * speedMultiplier));
}

function getResearchHint(selectedIds, maid) {
  if (maid.stats.research < 4) {
    return "";
  }

  const lockedMenus = MENUS.filter((menu) => !menu.unlocked);
  let bestMatch = null;

  lockedMenus.forEach((menu) => {
    const recipe = RECIPE_DEVELOPMENT.find((entry) => entry.menuId === menu.id);
    if (!recipe) {
      return;
    }

    const overlap = recipe.ingredients.filter((ingredientId) => selectedIds.includes(ingredientId));
    if (!bestMatch || overlap.length > bestMatch.overlap.length) {
      bestMatch = { recipe, overlap };
    }
  });

  if (!bestMatch || bestMatch.overlap.length === 0) {
    return "まだ新しい組み合わせを試す余地がありそうです。";
  }

  if (maid.stats.research === 4) {
    const overlapNames = bestMatch.overlap
      .map((ingredientId) => getIngredientById(ingredientId)?.name || ingredientId)
      .join(" / ");
      return `開発ヒント: 「${overlapNames}の組み合わせは悪くなさそう」`;
  }

  const missingIngredientId = bestMatch.recipe.ingredients.find((ingredientId) => !selectedIds.includes(ingredientId));
  const missingIngredient = missingIngredientId ? getIngredientById(missingIngredientId) : null;
  if (!missingIngredient) {
    return "いくつかの素材は悪くなさそうです。";
  }

  return `開発ヒント: 「${missingIngredient.name}を足すとひらめきそう」`;
}

function getResearchTaskDuration(maid, ingredientCount) {
  if (ingredientCount <= 1) {
    return 60;
  }
  const baseMinutes = (ingredientCount - 1) * 60;
  const researchStat = maid?.stats?.research || 1;
  // ここでレシピ開発能力に応じて、開発時間を少しずつ短縮します。
  const researchSpeedBonus = Math.max(0, researchStat - 1) * 0.04;
  return Math.max(60, Math.round(baseMinutes * (1 - researchSpeedBonus)));
}

function getResearchTask(maidId) {
  const maid = getMaidById(maidId);
  if (!maid?.task || maid.task.type !== "researchRecipe") {
    return null;
  }
  return maid.task;
}

function getTableMealScale(menuId) {
  return TABLE_MEAL_SCALES[menuId] ?? 0.56;
}

function completeResearchTask(maid, task) {
  const usedIngredients = task.usedIngredients || [];
  if (!state.isRecipeModalOpen) {
    state.hasRecipeDevelopmentNotice = true;
  }
  if (!task.matchedRecipe) {
    const hint = getResearchHint(usedIngredients, maid);
    state.recipeResult = `開発失敗。${maidLabel(maid)}が数時間かけて試しましたが、新レシピは生まれませんでした。${hint ? ` ${hint}` : ""}`;
    state.recipeResultImage = "";
    pushLog(`${maidLabel(maid)}がレシピ開発に失敗しました。使用食材: ${usedIngredients.map((id) => getIngredientById(id)?.name || id).join(" / ")}`, "開発");
    addMaidExp(maid, "research", 1);
    changeMaidAffection(maid.id, 1, "とレシピ開発を進めました。");
  } else {
    const menu = getMenuById(task.matchedRecipe.menuId);
    state.recipeResultImage = menu?.icon || "";
    if (menu?.unlocked) {
      state.recipeResult = `${maidLabel(maid)}が確認しました。${menu.name} はすでに開発済みです。`;
    } else if (menu) {
      menu.unlocked = true;
      menu.isPublished = true;
      state.lastUnlockedMenuId = menu.id;
      state.lastUnlockedMenuDay = state.day;
      const reputationGain = Math.max(0, maid.stats.research - 3);
      state.recipeResult = `${maidLabel(maid)}が${menu.name}を完成させました。メニューに追加されます。`;
      pushLog(`${maidLabel(maid)}が${menu.name}のレシピを開発しました。使用食材: ${usedIngredients.map((id) => getIngredientById(id)?.name || id).join(" / ")}`, "開発成功");
      addMaidExp(maid, "research", 2);
      changeMaidAffection(maid.id, 3, "と一緒にレシピ開発を成功させました。");
      if (reputationGain > 0) {
        changeReputation(reputationGain, "新作メニューの話題でお店の注目が高まりました。");
      }
    }
  }

  maid.task = null;
  maid.bubbleText = isOpen() ? TEXT.looking : TEXT.closingWork;
  moveMaid(maid, maid.homeX, maid.homeY);
  if (state.isRecipeBookOpen) {
    renderRecipeBook();
  }
  renderRecipeDevelopment();
  renderLog();
  updateActionButtonBadges();
}

function pauseResearchTasksForNextDay() {
  state.maids.forEach((maid) => {
    const task = maid.task;
    if (!task || task.type !== "researchRecipe" || task.paused) {
      return;
    }
    task.remainingMinutes = Math.max(1, task.endsAt - state.currentMinute);
    task.paused = true;
    maid.bubbleText = "レシピ開発を中断中";
    pushLog(`${maidLabel(maid)}のレシピ開発は閉店のため中断され、翌日に持ち越されます。`, "開発");
  });
}

function resumeResearchTasksForNewDay() {
  state.maids.forEach((maid) => {
    const task = maid.task;
    if (!task || task.type !== "researchRecipe" || !task.paused) {
      return;
    }
    task.paused = false;
    task.startedAt = state.currentMinute;
    task.endsAt = state.currentMinute + Math.max(1, task.remainingMinutes || 60);
    maid.bubbleText = "レシピ開発中";
    moveMaid(maid, maid.homeX, maid.homeY);
    pushLog(`${maidLabel(maid)}がレシピ開発を再開しました。完了予定 ${formatTime(task.endsAt)}`, "開発再開");
  });
}

function getUnlockedMenus() {
  return MENUS.filter((menu) => menu.unlocked);
}

function getPublishedMenus() {
  return MENUS.filter((menu) => menu.unlocked && menu.isPublished);
}

function getRecipeBookMenus() {
  return MENUS
    .filter((menu) => menu.unlocked || isInitialMenu(menu.id))
    .sort((a, b) => {
      const aInitial = isInitialMenu(a.id) ? 0 : 1;
      const bInitial = isInitialMenu(b.id) ? 0 : 1;
      if (aInitial !== bInitial) {
        return aInitial - bInitial;
      }
      return (a.recommendedReputation ?? 0) - (b.recommendedReputation ?? 0);
    });
}

function clamp(value, min, max) {
  return Math.min(max, Math.max(min, value));
}

function getTotalFixedCosts() {
  return getFixedCostsForEndMinute(CLOSE_MINUTE);
}

function getFixedCostsForEndMinute(endMinute) {
  const workedMinutes = Math.max(0, endMinute - OPEN_MINUTE);
  const wageCost = Math.round((workedMinutes / 60) * FIXED_COSTS.hourlyWage * FIXED_COSTS.maidCount);
  return FIXED_COSTS.dailyRent + wageCost;
}

function rollIngredientMarket() {
  INGREDIENTS.forEach((ingredient) => {
    const swing = 0.82 + Math.random() * 0.42;
    state.ingredientMarket[ingredient.id] = Math.max(10, Math.round(ingredient.price * swing));
  });
}

function chooseCustomerType() {
  let candidates = CUSTOMER_TYPES;
  if (state.currentMinute >= 12 * 60 && state.currentMinute < 13 * 60) {
    candidates = CUSTOMER_TYPES.filter((type) => type.id !== "couple");
  } else if (state.currentMinute >= 14 * 60 + 30 && state.currentMinute < 15 * 60 + 30) {
    candidates = CUSTOMER_TYPES.filter((type) => type.id !== "office-worker");
  } else if (state.currentMinute >= 18 * 60 && state.currentMinute < 19 * 60) {
    candidates = CUSTOMER_TYPES.filter((type) => type.id !== "student");
  }
  const weightedCandidates = candidates.map((type) => ({
    type,
    weight: Math.max(0.1, 1 + getPromotionCustomerTypeWeightBonus(type.id)),
  }));
  const totalWeight = weightedCandidates.reduce((sum, entry) => sum + entry.weight, 0);
  let roll = Math.random() * totalWeight;
  for (const entry of weightedCandidates) {
    roll -= entry.weight;
    if (roll <= 0) {
      return entry.type;
    }
  }
  return weightedCandidates[weightedCandidates.length - 1]?.type || CUSTOMER_TYPES[0];
}

function getPopularityLabel(popularity) {
  if (popularity >= 10) {
    return "大人気";
  }
  if (popularity >= 7) {
    return "人気";
  }
  if (popularity >= 4) {
    return "安定";
  }
  return "発掘中";
}

function getPhaseLabel(phase) {
  if (phase === "initial") {
    return "初期";
  }
  if (phase === "mid") {
    return "中盤";
  }
  if (phase === "late") {
    return "後半";
  }
  return "";
}

function getGenreLabel(genre) {
  if (genre === "meal") {
    return "食事";
  }
  if (genre === "drink") {
    return "飲み物";
  }
  return "デザート";
}

function getTimeDemandMultiplier() {
  if (state.currentMinute >= 12 * 60 && state.currentMinute < 13 * 60) {
    return 0.52;
  }
  if (state.currentMinute >= 14 * 60 + 30 && state.currentMinute < 15 * 60 + 30) {
    return 0.68;
  }
  if (state.currentMinute >= 18 * 60 && state.currentMinute < 19 * 60) {
    return 0.58;
  }
  if (state.currentMinute >= 11 * 60 && state.currentMinute < 12 * 60) {
    return 0.98;
  }
  if (state.currentMinute >= 13 * 60 && state.currentMinute < 14 * 60 + 30) {
    return 1.06;
  }
  if (state.currentMinute >= 15 * 60 + 30 && state.currentMinute < 18 * 60) {
    return 1.02;
  }
  if (state.currentMinute >= 19 * 60 && state.currentMinute < 20 * 60) {
    return 0.9;
  }
  return 1.24;
}

function getGuestInterval() {
  // 評判30を超えたら「低評判の強い来客抑制」から抜け始めるようにします。
  // 以前は評判50まで強く抑制され、30前後でも客足が極端に鈍く見えやすい状態でした。
  const reputationFactor = state.reputation < 30
    ? clamp(8 + (30 - state.reputation) * 0.45, 8, 22)
    : clamp(8 - (state.reputation - 30) * 0.35, -12, 8);
  const menuPopularityFactor = clamp(getPublishedMenus().reduce((sum, menu) => sum + menu.popularity, 0) / 6, 0, 8);
  const facilityFactor = state.facilities.ambiance * 1.5 + state.facilities.seating;
  const promotionIntervalCut = clamp(getPromotionVisitBonus() * 12, 0, 6);
  const interval = (30 + reputationFactor + getFlameVisitIntervalPenalty() - menuPopularityFactor - facilityFactor - promotionIntervalCut) * getTimeDemandMultiplier();
  return clamp(Math.round(interval), 9, 42);
}

function getVisitChance() {
  const reputationBoost =
    // 評判30前後で「そこそこ知られてきた店」として来店確率が底上げされるようにします。
    clamp((state.reputation - 30) / 150, -0.16, 0.28) +
    state.facilities.ambiance * 0.03 +
    getPromotionVisitBonus() -
    getFlameVisitChancePenalty();

  if (state.currentMinute >= 12 * 60 && state.currentMinute < 13 * 60) {
    return clamp(0.7 + reputationBoost, 0.24, 0.98);
  }
  if (state.currentMinute >= 14 * 60 + 30 && state.currentMinute < 15 * 60 + 30) {
    return clamp(0.62 + reputationBoost, 0.22, 0.96);
  }
  if (state.currentMinute >= 18 * 60 && state.currentMinute < 19 * 60) {
    return clamp(0.66 + reputationBoost, 0.22, 0.97);
  }
  if (state.currentMinute >= 11 * 60 && state.currentMinute < 12 * 60) {
    return clamp(0.48 + reputationBoost, 0.16, 0.9);
  }
  if (state.currentMinute >= 13 * 60 && state.currentMinute < 14 * 60 + 30) {
    return clamp(0.32 + reputationBoost, 0.14, 0.82);
  }
  if (state.currentMinute >= 15 * 60 + 30 && state.currentMinute < 18 * 60) {
    return clamp(0.38 + reputationBoost, 0.14, 0.86);
  }
  if (state.currentMinute >= 19 * 60 && state.currentMinute < 20 * 60) {
    return clamp(0.48 + reputationBoost, 0.16, 0.9);
  }
  return clamp(0.18 + reputationBoost, 0.08, 0.65);
}

function buildWeightedMenus(customerType) {
  const publishedMenus = getPublishedMenus();
  const hasUnlockedNewRecipes = publishedMenus.some((menu) => !isInitialMenu(menu.id));
  return publishedMenus.map((menu) => {
    let weight = 3.2 + menu.popularity * 0.22;
    if (hasUnlockedNewRecipes) {
      // 後から解放したレシピを注文されやすくして、初期ドリンクだけが上位に残り続けないようにします。
      if (isInitialMenu(menu.id)) {
        weight *= 0.48;
      } else if (menu.phase === "mid") {
        weight += 1.4;
      } else if (menu.phase === "late") {
        weight += 2.0;
      } else {
        weight += 0.9;
      }
    }
    if (menu.id === "omurice") {
      // 食事カテゴリの看板メニューとして、他の食事より選ばれやすくします。
      weight += 3.2;
    }
    if (state.currentMinute >= 12 * 60 && state.currentMinute < 14 * 60 && menu.genre === "meal") {
      weight += 1.0;
    }
    if (state.currentMinute >= 15 * 60 && state.currentMinute < 18 * 60 && menu.genre === "dessert") {
      weight += 1.0;
    }
    if (
      ["special-pancake", "choco-banana-crepe", "choco-ice-pancake", "choco-banana-ice-crepe", "fruit-ice-parfait", "pudding-a-la-mode"].includes(menu.id) &&
      state.currentMinute >= 15 * 60 &&
      state.currentMinute < 18 * 60
    ) {
      weight += 1.1;
    }
    if (customerType?.orderBias?.[menu.id]) {
      weight += customerType.orderBias[menu.id] * 0.75;
    }
    weight += getPromotionMenuWeightBonus(menu, customerType);
    return { menu, weight: Math.max(1, weight) };
  });
}

function chooseWeightedMenu(weightedMenus) {
  if (weightedMenus.length === 0) {
    return null;
  }

  const totalWeight = weightedMenus.reduce((sum, entry) => sum + entry.weight, 0);
  let roll = Math.random() * totalWeight;
  for (const entry of weightedMenus) {
    roll -= entry.weight;
    if (roll <= 0) {
      return entry.menu;
    }
  }
  return weightedMenus[weightedMenus.length - 1]?.menu || null;
}

function chooseMenusForCustomer(customerType) {
  const availableMenus = getPublishedMenus();
  if (availableMenus.length === 0) {
    return [];
  }

  const weightedMenus = buildWeightedMenus(customerType);
  const selectedGenreCounts = {};
  const orders = [];
  const initialMultiOrderChance =
    customerType?.id === "couple" ? 0.9 :
    customerType?.id === "otaku" ? 0.72 :
    customerType?.id === "student" ? 0.56 :
    0.48;
  const genreLimit = customerType?.id === "couple" ? 2 : 1;
  const maxOrdersByGenre = availableMenus.reduce((sum, menu) => {
    if (!sum.seen.has(menu.genre)) {
      sum.total += genreLimit;
      sum.seen.add(menu.genre);
    }
    return sum;
  }, { total: 0, seen: new Set() });
  const maxOrders = Math.min(6, maxOrdersByGenre.total);
  const minimumOrders = customerType?.id === "couple" ? Math.min(2, maxOrders) : 1;
  const targetCount = Math.max(
    minimumOrders,
    1 + (Math.random() < initialMultiOrderChance ? 1 : 0) + (Math.random() < initialMultiOrderChance * 0.5 ? 1 : 0)
  );

  while (orders.length < Math.min(targetCount, maxOrders)) {
    const orderedMenuIds = new Set(orders.map((menu) => menu.id));
    const candidates = weightedMenus.filter((entry) => (selectedGenreCounts[entry.menu.genre] || 0) < genreLimit && !orderedMenuIds.has(entry.menu.id));
    const selected = chooseWeightedMenu(candidates);
    if (!selected) {
      break;
    }
    orders.push(selected);
    selectedGenreCounts[selected.genre] = (selectedGenreCounts[selected.genre] || 0) + 1;
  }

  return orders;
}

function getCustomerTargetStayMinutes(customer) {
  const orders = customer?.orders || [];
  const drinkCount = orders.filter((menu) => menu.genre === "drink").length;
  const dessertCount = orders.filter((menu) => menu.genre === "dessert").length;
  const mealCount = orders.filter((menu) => menu.genre === "meal").length;
  const hasDrink = drinkCount > 0;
  const hasDessert = dessertCount > 0;
  const hasMeal = mealCount > 0;

  let targetMinutes = 30;
  if (hasMeal && hasDessert && hasDrink) {
    targetMinutes = 60;
  } else if (hasMeal && hasDrink) {
    targetMinutes = 45;
  } else if (hasDessert && hasDrink) {
    targetMinutes = 35;
  } else if (hasMeal && hasDessert) {
    targetMinutes = 50;
  } else if (hasMeal) {
    targetMinutes = 38;
  } else if (hasDessert) {
    targetMinutes = 28;
  } else if (hasDrink) {
    targetMinutes = 25;
  }

  // ここで食事かデザートを含む注文は、少しだけ長めに滞在するよう時間を加算します。
  if (hasMeal || hasDessert) {
    targetMinutes += 5;
  }

  targetMinutes += Math.max(0, mealCount - 1) * 6;
  targetMinutes += Math.max(0, dessertCount - (hasDrink ? 1 : 0)) * 4;
  targetMinutes += Math.max(0, drinkCount - 1) * 2;

  const customerTypeId = customer?.customerType?.id;
  const typeMultiplier =
    customerTypeId === "office-worker" ? 0.92 :
    customerTypeId === "otaku" ? 1.08 :
    customerTypeId === "couple" ? 1.08 :
    1;

  return Math.round(targetMinutes * typeMultiplier);
}

function scheduleNextGuest(extraDelay = 0) {
  state.nextGuestAt = state.currentMinute + getGuestInterval() + extraDelay;
}

function enqueueWaitingCustomer(customer) {
  customer.queuedAt = state.currentMinute;
  state.waitingCustomers.push(customer);
  pushLog(`${customer.customerType.name}のお客さんが行列に並びました。`, "行列");
}

function handleSeatedNoPreference(table, customer) {
  if (!table || !customer || hasPreferredPublishedMenu(customer.customerType)) {
    return false;
  }

  if (!shouldLeaveWithoutPreferredMenu(customer.customerType)) {
    return false;
  }

  customer.state = "reviewingMenu";
  customer.leaveAt = state.currentMinute + 7;
  table.statusText = TEXT.noPreference;
  pushLog(`${getCustomerLogName(customer)}が席につきましたが、好みのメニューが見当たらず悩んでいます。`, "検討中");
  return true;
}

function seatWaitingCustomerIfPossible() {
  if (state.waitingCustomers.length === 0) {
    return;
  }
  const emptyTable = getActiveTables().find((table) => !table.occupant && !table.dirty && !table.pendingCleanup && !table.waitingForSales);
  if (!emptyTable) {
    return;
  }
  const customer = state.waitingCustomers.shift();
  customer.tableId = emptyTable.id;
  const waitedMinutes = customer.queuedAt != null ? state.currentMinute - customer.queuedAt : 0;
  customer.mood -= Math.floor(waitedMinutes / 6);
  customer.satisfaction -= Math.floor(waitedMinutes / 8);
  emptyTable.occupant = customer;
  emptyTable.statusText = `${customer.customerType.name} / ${TEXT.seated}`;
  customer.state = "waitingOrder";
  customer.arrivedAt = state.currentMinute;
  pushLog(`${getCustomerLogName(customer)}を席へご案内しました。`, "案内");
  if (handleSeatedNoPreference(emptyTable, customer)) {
    return;
  }
  queueTask("takeOrder", emptyTable.id);
}

function updateWaitingCustomers() {
  if (state.waitingCustomers.length === 0) {
    return;
  }

  const remaining = [];
  state.waitingCustomers.forEach((customer) => {
    const waitedMinutes = customer.queuedAt != null ? state.currentMinute - customer.queuedAt : 0;
    if (waitedMinutes > 0 && waitedMinutes % 8 === 0) {
      customer.mood -= 1;
      customer.satisfaction -= 1;
    }

    if (waitedMinutes >= customer.waitingLimit || customer.mood <= -3) {
      pushLog(`${customer.customerType.name}のお客さんは席待ちで疲れて帰ってしまいました。`, "行列離脱");
      state.dailyWalkouts += 1;
      changeReputation(-1, "席待ちでお客さんを離脱させてしまいました。");
      return;
    }

    remaining.push(customer);
  });

  state.waitingCustomers = remaining;
}

function createDailyReport() {
  const dailyMenuOrders = state.dailyMenuOrders || {};
  const orderedMenus = Object.entries(dailyMenuOrders)
    .map(([menuId, count]) => {
      const menu = getMenuById(menuId);
      return menu && count > 0 ? { menu, count } : null;
    })
    .filter(Boolean);
  const popularityRanking = orderedMenus.length > 0
    ? orderedMenus
      .sort((a, b) => b.count - a.count || b.menu.popularity - a.menu.popularity || a.menu.name.localeCompare(b.menu.name, "ja"))
      .slice(0, 3)
      .map(({ menu, count }) => ({
        name: menu.name,
        icon: menu.icon || getMenuIcon(menu.id),
        popularity: menu.popularity,
        label: getPopularityLabel(menu.popularity),
        dailyCount: count,
      }))
    : [...getUnlockedMenus()]
      .sort((a, b) => b.popularity - a.popularity)
      .slice(0, 3)
      .map((menu) => ({
        name: menu.name,
        icon: menu.icon || getMenuIcon(menu.id),
        popularity: menu.popularity,
        label: getPopularityLabel(menu.popularity),
        dailyCount: 0,
      }));

  const reputationDelta = state.reputation - state.dayStartReputation;
  return {
    day: state.day,
    sales: state.dailySales,
    guests: state.dailyGuests,
    walkouts: state.dailyWalkouts,
    avgSatisfaction: state.dailyFinishedGroups > 0 ? Math.round((state.dailySatisfactionTotal / state.dailyFinishedGroups) * 10) / 10 : 0,
    reputationDelta,
    money: state.money,
    fixedCosts: state.dailyFixedCosts,
    profit: state.dailySales - state.dailyFixedCosts,
    popularityRanking,
  };
}

function updateStoryProgressStatsForDay() {
  const profit = state.dailySales - state.dailyFixedCosts;
  if (profit >= 0) {
    state.profitableDays += 1;
    state.consecutiveDeficitDays = 0;
    if (state.hadSnsFlame) {
      state.postFlameProfitableDays += 1;
    }
  } else {
    state.consecutiveDeficitDays += 1;
    if (state.hadSnsFlame) {
      state.postFlameProfitableDays = 0;
    }
  }
}

function createDeliveryReport(delivery) {
  const items = Object.entries(delivery.items).map(([ingredientId, amount]) => {
    const ingredient = getIngredientById(ingredientId);
    return {
      id: ingredientId,
      name: ingredient?.name || ingredientId,
      icon: getIngredientIcon(ingredientId),
      amount,
    };
  });

  return {
    day: state.day,
    orderedOnDay: delivery.orderedOnDay,
    arrivalDay: delivery.arrivalDay,
    totalCost: delivery.totalCost,
    moneyAfterPayment: state.money,
    items,
  };
}

function getDailySummary(report) {
  if (report.profit >= 3000 && report.reputationDelta >= 2 && report.avgSatisfaction >= 2) {
    return "好調な1日でした。売上と評判が両方伸び、明日の来客にも良い影響が期待できます。";
  }
  if (report.walkouts >= 3) {
    return "席待ちや好みミスマッチで離脱が目立ちました。行列対策とメニュー公開構成の見直しが効きそうです。";
  }
  if (report.profit >= 1000 && report.avgSatisfaction >= 0) {
    return "安定した営業でした。人気メニューと評判の推移を見ながら、次の1日にもつなげられます。";
  }
  if (report.guests <= 2 || report.reputationDelta < 0 || report.profit < 0 || report.avgSatisfaction < 0) {
    return "客足が伸び悩みました。評判と人気メニューを整えると、明日は改善しやすくなります。";
  }
  return "よく頑張った1日でした。人気と在庫を見ながら、次の営業でも安定した運営を目指せます。";
}

function advanceToNextDay() {
  state.day += 1;
  rollIngredientMarket();
  state.currentMinute = START_MINUTE;
  state.dailySales = 0;
  state.dailyMenuOrders = {};
  state.dailyGuests = 0;
  state.dailyFixedCosts = getTotalFixedCosts();
  state.dailyWalkouts = 0;
  state.dailySatisfactionTotal = 0;
  state.dailyFinishedGroups = 0;
  state.nextGuestAt = OPEN_MINUTE + 8;
  state.cycleStep = null;
  state.activeKitchenOrder = null;
  state.closingLogged = false;
  state.daySettled = false;
  state.nextDayQueued = false;
  state.lastDailyReport = null;
  state.dayStartMoney = state.money;
  state.dayStartReputation = state.reputation;
  state.waitingCustomers = [];
  state.taskQueue = [];
  state.hasOrderedToday = false;
  state.hasPromotedToday = false;
  state.pendingInventoryOrder = {};
  if (state.isFlamePenaltyActive && !state.isChapterLockedFlamePenalty && state.flamePenaltyDaysRemaining > 0) {
    state.flamePenaltyDaysRemaining -= 1;
    if (state.flamePenaltyDaysRemaining <= 0) {
      state.isFlamePenaltyActive = false;
      pushLog("炎上による悪影響が少しずつ落ち着いてきました。", "沈静化");
    }
  }
  advancePromotionDurations();
  state.tables.forEach((table) => {
    table.occupant = null;
    table.settledMenus = [];
    table.mealServed = false;
    table.dirty = false;
    table.waitingForSales = false;
    table.pendingCleanup = false;
    table.salesCustomerTypeId = null;
    table.statusText = TEXT.empty;
    table.visualMeals = [];
    table.visualDish = false;
  });
  state.maids.forEach((maid) => {
    if (maid.task?.type === "researchRecipe" && maid.task.paused) {
      maid.bubbleText = "レシピ開発を中断中";
      moveMaid(maid, maid.homeX, maid.homeY);
      return;
    }
    maid.task = null;
    maid.bubbleText = TEXT.idle;
    moveMaid(maid, maid.homeX, maid.homeY);
  });

  resumeResearchTasksForNewDay();
  pushLog(`${state.day}${TEXT.daySuffix}の開店準備を始めます。`, "翌日");
  pushLog(`本日の固定費は${yen(state.dailyFixedCosts)}です。`, "経費");
  captureDayStartCheckpoint();
  processMorningStart();
}

function normalizedIngredientKey(ids) {
  return [...ids].sort().join("|");
}

function sanitizeLogMessage(message) {
  return String(message || "").replace(/\b(?:A-[123]|B-[23])\s*(?:の|に|では|で|へ)?\s*/g, "");
}

function pushLog(message, strong = "") {
  state.logEntries.unshift({ message: sanitizeLogMessage(message), strong, key: `${Date.now()}-${Math.random()}` });
  state.logEntries = state.logEntries.slice(0, 8);
}

function queueSystemNotice(id, title, body) {
  if (!id || state.shownSystemNoticeIds.includes(id) || state.systemNoticeQueue.some((notice) => notice.id === id)) {
    return;
  }
  state.systemNoticeQueue.push({ id, title, body });
}

function canOpenSystemNotice() {
  return !state.isTitleScreenOpen &&
    !state.isEndingThanksScreenOpen &&
    !state.isRecipeModalOpen &&
    !state.isRecipeBookOpen &&
    !state.isAdvModalOpen &&
    !state.isInventoryModalOpen &&
    !state.isMaidListOpen &&
    !state.isFacilityModalOpen &&
    !state.isPromotionModalOpen &&
    !state.isDailyReportOpen &&
    !state.isDeliveryReportOpen &&
    !state.isSystemNoticeOpen;
}

function maybeOpenSystemNotice() {
  if (!canOpenSystemNotice() || state.systemNoticeQueue.length === 0) {
    return false;
  }
  const notice = state.systemNoticeQueue.shift();
  state.activeSystemNotice = notice;
  state.isSystemNoticeOpen = true;
  if (DOM.systemNoticeTitle) {
    DOM.systemNoticeTitle.textContent = notice.title;
  }
  if (DOM.systemNoticeBody) {
    DOM.systemNoticeBody.textContent = notice.body;
  }
  DOM.systemNoticeModalBackdrop?.classList.remove("hidden");
  updateUI();
  return true;
}

function maybeOpenTrialEndDialog() {
  if (!canOpenSystemNotice() || state.trialEndShown || getCompletedMainChapterNumber() < 5) {
    return false;
  }
  return openSystemDialog(
    "体験版はここまでです",
    "ここまで遊んでいただきありがとうございます。\n\n体験版では第5章までお楽しみいただけます。\n製品版では、この先の本編シナリオ、個別ルート、各種エンディングを引き続きお楽しみいただけます。"
  );
}

function openSystemDialog(title, body) {
  if (!canOpenSystemNotice()) {
    return false;
  }
  const isTrialEndDialog = title === "体験版はここまでです";
  state.activeSystemNotice = { id: isTrialEndDialog ? "trial-end-message" : "", title, body, transient: true };
  state.isSystemNoticeOpen = true;
  if (DOM.systemNoticeTitle) {
    DOM.systemNoticeTitle.textContent = title;
  }
  if (DOM.systemNoticeBody) {
    DOM.systemNoticeBody.textContent = body;
  }
  DOM.systemNoticeModalBackdrop?.classList.remove("hidden");
  updateUI();
  return true;
}

function closeSystemNotice() {
  const notice = state.activeSystemNotice;
  if (notice?.id && !notice.transient && !state.shownSystemNoticeIds.includes(notice.id)) {
    state.shownSystemNoticeIds.push(notice.id);
  }
  state.activeSystemNotice = null;
  state.isSystemNoticeOpen = false;
  DOM.systemNoticeModalBackdrop?.classList.add("hidden");

  if (notice?.id === "trial-end-message") {
    state.trialEndShown = true;
    window.location.reload();
    return;
  }

  updateUI();
  if (maybeOpenTrialEndDialog()) {
    return;
  }
  if (maybeOpenSystemNotice()) {
    return;
  }
  if (state.pendingAdvanceAfterAdv) {
    state.pendingAdvanceAfterAdv = false;
    if (state.daySettled && state.nextDayQueued) {
      advanceToNextDay();
    }
  }
}

function unlockIngredientsByReputation() {
  let unlockedNewIngredient = false;
  INGREDIENTS.forEach((ingredient) => {
    if (state.reputation < ingredient.unlockReputation) {
      return;
    }
    if (state.unlockedIngredientIds.includes(ingredient.id)) {
      return;
    }
    state.unlockedIngredientIds.push(ingredient.id);
    state.inventory[ingredient.id] = state.inventory[ingredient.id] || ingredient.initial || 0;
    unlockedNewIngredient = true;
    pushLog(`${ingredient.name}が仕入れ可能になりました。(評判 ${ingredient.unlockReputation})`, "食材解放");
  });
  if (unlockedNewIngredient && !state.isInventoryModalOpen) {
    state.hasInventoryNotice = true;
    updateActionButtonBadges();
  }
}

function changeReputation(delta, reason = "") {
  const previousVisibleReputation = state.reputation;
  let adjustedDelta = delta;
  if (state.isFlamePenaltyActive) {
    // 炎上中は、通常炎上と特例炎上で評判補正を分けます。
    if (state.isChapterLockedFlamePenalty) {
      adjustedDelta = delta > 0 ? delta * 0.1 : delta * 10;
    } else {
      adjustedDelta = delta > 0 ? delta * 0.5 : delta * 3;
    }
  }
  let scaledDelta;
  if (adjustedDelta > 0) {
    // ここで評判が高くなるほど上がりにくくなる補正を入れます。
    const highReputationSlowdown = state.reputation >= 50
      ? clamp(1 - (state.reputation - 50) / 70, 0.48, 1)
      : 1;
    const growthScale = clamp(1 - state.reputation / 125, 0.22, 1) * highReputationSlowdown;
    scaledDelta = adjustedDelta * 0.225 * growthScale;
  } else {
    // ここで評判低下の幅を抑えて、一度の失敗で落ちすぎないようにします。
    scaledDelta = adjustedDelta * 0.14;
  }
  state.reputationValue = Math.max(0, Math.min(100, state.reputationValue + scaledDelta));
  state.reputation = Math.floor(state.reputationValue);
  unlockIngredientsByReputation();
  if (reason && state.reputation !== previousVisibleReputation) {
    const visibleDelta = state.reputation - previousVisibleReputation;
    const sign = visibleDelta > 0 ? "+" : "";
    pushLog(`${reason} 評判 ${sign}${visibleDelta}`, "評判");
  }
}

function changeMenuPopularity(menu, delta, reason = "") {
  if (!menu) {
    return;
  }
  menu.popularity = clamp(menu.popularity + delta, 1, 12);
  if (reason) {
    const sign = delta > 0 ? "+" : "";
    pushLog(`${menu.name}の人気 ${sign}${delta} ${reason}`, "人気");
  }
}

function changeMaidAffection(maidId, delta, reason = "") {
  if (!state.affection[maidId] && state.affection[maidId] !== 0) {
    state.affection[maidId] = 0;
  }
  state.affection[maidId] = Math.max(0, state.affection[maidId] + delta);
  const maid = getMaidById(maidId);
  if (maid && reason) {
    const sign = delta > 0 ? "+" : "";
    pushLog(`${maidLabel(maid)} ${reason} 好感度 ${sign}${delta}`, "好感度");
  }
}

function hasIngredientsForMenu(menu) {
  return Object.entries(menu.ingredients).every(([ingredientId, required]) => {
    return (state.inventory[ingredientId] || 0) >= required;
  });
}

function consumeIngredientsForMenu(menu) {
  Object.entries(menu.ingredients).forEach(([ingredientId, required]) => {
    state.inventory[ingredientId] = Math.max(0, (state.inventory[ingredientId] || 0) - required);
  });
}

function consumeSelectedIngredients(ingredientIds) {
  ingredientIds.forEach((ingredientId) => {
    state.inventory[ingredientId] = Math.max(0, (state.inventory[ingredientId] || 0) - 1);
  });
}

function maidLabel(maid) {
  return maid.name;
}

function setCycle(step) {
  state.cycleStep = step;
}

function queueTask(type, tableId) {
  const table = getTable(tableId);
  if (!table) {
    return;
  }

  // 片付けや配膳を優先して、営業の詰まりを減らします。
  const priorityMap = { cleanup: 0, serveMeal: 1, takeOrder: 2, cookMeal: 3 };
  const inProgress = state.maids.some((maid) => maid.task && maid.task.type === type && maid.task.tableId === tableId);
  if (inProgress) {
    return;
  }

  const alreadyQueued = state.taskQueue.some((task) => task.type === type && task.tableId === tableId);
  if (!alreadyQueued) {
    state.taskQueue.push({ type, tableId });
    state.taskQueue.sort((a, b) => priorityMap[a.type] - priorityMap[b.type]);
  }
}

function getMaidTaskPreferenceScore(maid, task, table) {
  const currentOrder = getCurrentCustomerOrder(table?.occupant);
  const statScore = task.type === "cookMeal" ? maid.stats.cooking * 1.4 : maid.stats.service * 1.1;
  const traitScore = maid.traits?.[task.type] ?? 0;
  const researchPenalty = state.isRecipeModalOpen && state.selectedResearchMaidId === maid.id ? -6 : 0;
  const orderPressureBonus =
    task.type === "takeOrder" && table?.occupant && state.currentMinute - table.occupant.arrivedAt > 10 ? 1.2 : 0;
  const cleanupBonus = task.type === "cleanup" && table?.dirty ? 1.6 : 0;
  const cookingBonus = task.type === "cookMeal" && currentOrder ? Math.max(0, currentOrder.cookTime - 10) * 0.04 : 0;
  const randomDrift = Math.random() * 1.5;
  return statScore + traitScore + researchPenalty + orderPressureBonus + cleanupBonus + cookingBonus + randomDrift;
}

function chooseMaidForTask(idleMaids, task, table) {
  if (idleMaids.length === 0) {
    return null;
  }

  return idleMaids.reduce((best, maid) => {
    const score = getMaidTaskPreferenceScore(maid, task, table);
    if (!best || score > best.score) {
      return { maid, score };
    }
    return best;
  }, null)?.maid || idleMaids[0];
}

function startNextTasks() {
  const idleMaids = [...state.maids.filter((maid) => !maid.task)];
  while (idleMaids.length > 0 && state.taskQueue.length > 0) {
    const nextTask = state.taskQueue.shift();
    const definition = TASK_DEFINITIONS[nextTask.type];
    const table = getTable(nextTask.tableId);
    if (!table || !definition) {
      continue;
    }

    const maid = chooseMaidForTask(idleMaids, nextTask, table);
    if (!maid) {
      break;
    }

    const target = getTaskTarget(nextTask.type, table);
    const duration = getMaidTaskDuration(maid, nextTask.type, table);
    maid.task = {
      ...nextTask,
      startedAt: state.currentMinute,
      endsAt: state.currentMinute + duration,
      bubble: definition.bubble,
      cycle: definition.cycle,
      target,
    };

    maid.bubbleText = definition.bubble;
    setCycle(definition.cycle);
    moveMaid(maid, target.x, target.y);
    const maidIndex = idleMaids.findIndex((idleMaid) => idleMaid.id === maid.id);
    if (maidIndex >= 0) {
      idleMaids.splice(maidIndex, 1);
    }
  }
}

function getTaskTarget(type, table) {
  if (type === "cookMeal") {
    // ここで調理時に移動する共通の調理位置を設定します。
    return { x: 50, y: 50 };
  }
  // ここで各席の接客位置へ移動する座標を設定します。
  return {
    x: table.x + table.serviceOffsetX,
    y: table.y + table.serviceOffsetY,
  };
}

function moveMaid(maid, x, y) {
  const layerWidth = DOM.maidsLayer.clientWidth || 760;
  const layerHeight = DOM.maidsLayer.clientHeight || 520;
  const maidWidth = 136;
  const maidHeight = 228;
  const minX = 18;
  const maxX = Math.max(minX, layerWidth - maidWidth - 18);
  const minY = 18;
  const maxY = Math.max(minY, layerHeight - maidHeight - 18);

  maid.x = Math.min(Math.max(x, minX), maxX);
  maid.y = Math.min(Math.max(y, minY), maxY);
}

function maybeSpawnCustomer() {
  if (!isOpen() || state.currentMinute < state.nextGuestAt) {
    return;
  }

  if (getPublishedMenus().length === 0) {
    scheduleNextGuest(10);
    return;
  }

  const visitChance = getVisitChance();
  if (Math.random() > visitChance) {
    scheduleNextGuest(4);
    return;
  }

  const emptyTable = getActiveTables().find((table) => !table.occupant && !table.dirty && !table.pendingCleanup && !table.waitingForSales);
  if (!emptyTable) {
    if (state.waitingCustomers.length < getWaitingCapacity()) {
      const waitingCustomer = makeCustomer("waiting");
      if (waitingCustomer.orders.length) {
        state.guestCount += 1;
        state.dailyGuests += 1;
        enqueueWaitingCustomer(waitingCustomer);
      }
    } else {
      pushLog("満席と行列で、お客さんが諦めて帰りました。", "機会損失");
    }
    scheduleNextGuest(3);
    return;
  }

  const customer = makeCustomer(emptyTable.id);
  if (!customer.orders.length) {
    scheduleNextGuest(10);
    return;
  }
  state.guestCount += 1;
  state.dailyGuests += 1;
  emptyTable.occupant = customer;
  emptyTable.statusText = `${customer.customerType.name} / ${TEXT.seated}`;
  customer.state = "waitingOrder";
  scheduleNextGuest(Math.floor(Math.random() * 5));
  setCycle("arrive");
  pushLog(`${getCustomerLogName(customer)}が来店しました。注文予定: ${getCustomerOrderSummary(customer)}`, "来店");
  if (handleSeatedNoPreference(emptyTable, customer)) {
    scheduleNextGuest(6);
    return;
  }
  queueTask("takeOrder", emptyTable.id);
}

function updateEatingCustomers() {
  state.tables.forEach((table) => {
    const customer = table.occupant;
    if (!customer) {
      return;
    }

      if (customer.state === "reviewingMenu" && state.currentMinute >= customer.leaveAt) {
        state.dailyWalkouts += 1;
        changeReputation(-1, "好みのメニューが無く、着席後にお客さんが退店しました。");
        customer.state = "leaving";
        table.occupant = null;
        table.statusText = TEXT.empty;
        setCycle("leave");
        pushLog(`${getCustomerLogName(customer)}は好みのメニューが無いため退店しました。`, "退店");
        return;
      }

      if (customer.state === "eating" && state.currentMinute >= customer.eatUntil) {
        const extraMenu = maybeAppendExtraOrder(customer);
        if (extraMenu) {
          customer.state = "ordered";
          customer.satisfaction += 1;
          table.statusText = `${extraMenu.name}${TEXT.cooking}`;
          pushLog(`${getCustomerLogName(customer)}が追加注文で ${extraMenu.name} を選びました。`, "追加注文");
          queueTask("cookMeal", table.id);
          return;
        }
        customer.state = "finished";
        customer.leaveAt = state.currentMinute + Math.max(3, customer.postMealRestMinutes || 4);
        table.statusText = TEXT.finished;
        setCycle("eat");
        pushLog(`${getCustomerLogName(customer)}が食事を終えました。`, "完食");
      }

      if (customer.state === "finished" && state.currentMinute >= customer.leaveAt) {
        const serviceDuration = customer.servedAt ? customer.servedAt - customer.arrivedAt : 999;
        const customerType = customer.customerType || getCustomerTypeById("student");
        const adjustedDuration = serviceDuration / (customerType?.serviceTolerance || 1);
        const positiveMod = customerType?.reputationMod || 0;
        let satisfactionScore = customer.satisfaction;
        // ここで妥協して別メニューに切り替えた回数ぶん、満足度を下げやすくしています。
        satisfactionScore -= (customer.compromiseCount || 0) * 2;
        if (adjustedDuration <= 55) {
          satisfactionScore += 2;
        } else if (adjustedDuration <= 85) {
          satisfactionScore += 1;
        } else {
          satisfactionScore -= 2;
        }

        if (satisfactionScore >= 2) {
          changeReputation(2 + Math.max(0, positiveMod), `${customerType.name}のお客さんが接客にとても満足しました。`);
          customer.orders.forEach((menu) => {
            changeMenuPopularity(menu, 1, "満足度の高い提供で評価が上がりました。");
          });
        } else if (satisfactionScore >= 0) {
          changeReputation(Math.max(0, 1 + positiveMod), `${customerType.name}のお客さんに満足してもらいました。`);
        } else {
          changeReputation(-1 + Math.min(0, positiveMod), `${customerType.name}のお客さんにとって提供が遅く感じられました。`);
          customer.orders.forEach((menu) => {
            changeMenuPopularity(menu, -1, "提供の遅さで注目が下がりました。");
          });
        }
      state.dailySatisfactionTotal += satisfactionScore;
      state.dailyFinishedGroups += 1;

        customer.state = "leaving";
      table.occupant = null;
        table.dirty = true;
        table.pendingCleanup = true;
        table.waitingForSales = true;
        table.salesCustomerTypeId = customer.customerType?.id || null;
        table.visualDish = true;
        table.visualMeals = [];
        table.statusText = TEXT.dirty;
      setCycle("leave");
      pushLog(`${getCustomerLogName(customer)}が退店しました。`, "退店");
      queueTask("cleanup", table.id);
    }
  });
}

function applyFinishedTasks() {
  state.maids.forEach((maid) => {
    const task = maid.task;
    if (!task || state.currentMinute < task.endsAt) {
      return;
    }

    if (task.type === "researchRecipe") {
      completeResearchTask(maid, task);
      return;
    }

    const table = getTable(task.tableId);
    if (!table) {
      maid.task = null;
      maid.bubbleText = isOpen() ? TEXT.looking : TEXT.closingWork;
      return;
    }

      if (task.type === "takeOrder" && table.occupant) {
        table.occupant.state = "ordered";
        table.occupant.orderedAt = state.currentMinute;
        table.occupant.satisfaction += 1;
        table.occupant.currentOrderIndex = 0;
      const firstOrder = getCurrentCustomerOrder(table.occupant);
      table.statusText = `${getCustomerOrderSummary(table.occupant)}${TEXT.cooking}`;
      pushLog(`${maidLabel(maid)}が ${getCustomerOrderSummary(table.occupant)} の注文を受けました。`, "注文");
      addMaidExp(maid, "service", 1);
      if (!firstOrder) {
        maid.task = null;
        maid.bubbleText = isOpen() ? TEXT.looking : TEXT.closingWork;
        moveMaid(maid, maid.homeX, maid.homeY);
        return;
      }
      queueTask("cookMeal", table.id);
    }

      if (task.type === "cookMeal" && table.occupant) {
        const pendingOrders = getPendingCustomerOrders(table.occupant);
        if (pendingOrders.length === 0) {
          maid.task = null;
          maid.bubbleText = isOpen() ? TEXT.looking : TEXT.closingWork;
          moveMaid(maid, maid.homeX, maid.homeY);
          return;
        }
        const preparedMenus = [];
        for (let index = table.occupant.currentOrderIndex; index < table.occupant.orders.length; index += 1) {
          let order = table.occupant.orders[index];
          const requestedMenu = order;
          if (!hasIngredientsForMenu(order)) {
            const compromiseMenu = findCompromiseMenuForCustomer(table.occupant, order, index);
            if (compromiseMenu) {
              table.occupant.orders[index] = compromiseMenu;
              order = compromiseMenu;
              table.occupant.satisfaction -= 2;
              table.occupant.mood -= 1;
              table.occupant.compromiseCount = (table.occupant.compromiseCount || 0) + 1;
              changeMenuPopularity(requestedMenu, -1, "材料不足で別メニューへ案内しました。");
              pushLog(`${requestedMenu.name}の材料が不足したため、代わりに ${compromiseMenu.name} をお出しすることになりました。`, "代替注文");
            } else {
              pushLog(`${order.name}の材料が不足し、お客さんが怒って帰ってしまいました。`, "材料不足");
              changeReputation(-4, "材料不足で提供できませんでした。");
              changeMenuPopularity(order, -1, "材料不足で提供できませんでした。");
              table.occupant = null;
              table.settledMenus = [];
              table.statusText = TEXT.empty;
              table.visualMeals = [];
              table.visualDish = false;
              table.pendingServedMenus = [];
              setCycle("leave");
              maid.task = null;
              maid.bubbleText = isOpen() ? TEXT.looking : TEXT.closingWork;
              moveMaid(maid, maid.homeX, maid.homeY);
              return;
            }
          }
          consumeIngredientsForMenu(order);
          preparedMenus.push(order);
        }
        table.occupant.satisfaction += 1;
        table.statusText = `${preparedMenus.map((menu) => menu.name).join(" / ")}${TEXT.waitServe}`;
        state.activeKitchenOrder = {
          tableId: table.id,
          icon: preparedMenus[0]?.icon || "",
          label: preparedMenus.map((menu) => menu.name).join(" / "),
        };
        table.pendingServedMenus = preparedMenus;
        pushLog(`${maidLabel(maid)}が ${preparedMenus.map((menu) => menu.name).join(" / ")} をまとめて作り上げました。`, "調理完了");
        addMaidExp(maid, "cooking", 1);
        queueTask("serveMeal", table.id);
      }

    if (task.type === "serveMeal" && table.occupant) {
      const preparedMenus = table.pendingServedMenus || getPendingCustomerOrders(table.occupant);
      if (!preparedMenus.length) {
        maid.task = null;
        maid.bubbleText = isOpen() ? TEXT.looking : TEXT.closingWork;
        moveMaid(maid, maid.homeX, maid.homeY);
        return;
      }
        table.occupant.servedAt ??= state.currentMinute;
        table.occupant.satisfaction += 1;
        table.settledMenus.push(...preparedMenus);
      table.mealServed = true;
      table.visualMeals = [
        ...table.visualMeals,
        ...preparedMenus.map((menu) => ({
          id: menu.id,
          genre: menu.genre,
          icon: menu.icon,
          scale: getTableMealScale(menu.id),
        })),
      ].slice(0, 3);
      if (state.activeKitchenOrder && state.activeKitchenOrder.tableId === table.id) {
        state.activeKitchenOrder = null;
      }
      setCycle("serve");
      pushLog(`${maidLabel(maid)}が ${preparedMenus.map((menu) => menu.name).join(" / ")} をまとめて提供しました。`, "配膳");
      addMaidExp(maid, "service", 1);
      table.occupant.currentOrderIndex += preparedMenus.length;
      table.pendingServedMenus = [];

      if (table.occupant.currentOrderIndex < table.occupant.orders.length) {
        const nextOrders = getPendingCustomerOrders(table.occupant);
        table.statusText = `${nextOrders.map((menu) => menu.name).join(" / ")}${TEXT.cooking}`;
        queueTask("cookMeal", table.id);
      } else {
          const customerType = table.occupant.customerType || getCustomerTypeById("student");
          table.occupant.state = "eating";
          // ここで来店から退店までの目標滞在時間に合わせて、残りの食事時間と休憩時間を設定します。
          const targetStayMinutes = getCustomerTargetStayMinutes(table.occupant);
          const elapsedMinutes = Math.max(0, state.currentMinute - table.occupant.arrivedAt);
          const remainingStayMinutes = Math.max(10, targetStayMinutes - elapsedMinutes);
          const postMealRestMinutes = Math.max(3, Math.min(6, Math.round(remainingStayMinutes * 0.15)));
          const eatingMinutes = Math.max(6, remainingStayMinutes - postMealRestMinutes);
          table.occupant.postMealRestMinutes = postMealRestMinutes;
          table.occupant.eatUntil = state.currentMinute + eatingMinutes;
          table.statusText = TEXT.eating;
        }
    }

    if (task.type === "cleanup") {
      table.pendingCleanup = false;
      table.dirty = false;
      table.visualDish = false;
      table.statusText = TEXT.cleaned;
      setCycle("clean");
        pushLog(`${maidLabel(maid)}が食器を片付けました。`, "片付け");
        addMaidExp(maid, "service", 1);
      }

    maid.task = null;
    maid.bubbleText = isOpen() ? TEXT.looking : TEXT.closingWork;
    moveMaid(maid, maid.homeX, maid.homeY);
  });
}

function settleSales() {
  state.tables.forEach((table) => {
    if (!table.waitingForSales || table.pendingCleanup) {
      return;
    }

    const settledMenus = [...table.settledMenus];
    table.waitingForSales = false;
    table.mealServed = false;
    table.statusText = TEXT.empty;
    table.settledMenus = [];
    if (settledMenus.length === 0) {
      return;
    }
    const salesMultiplier = getPromotionSalesMultiplier(table.salesCustomerTypeId);
    const totalSales = Math.round(settledMenus.reduce((sum, menu) => sum + menu.price, 0) * salesMultiplier);
    state.dailySales += totalSales;
    if (!state.dailyMenuOrders) {
      state.dailyMenuOrders = {};
    }
    settledMenus.forEach((menu) => {
      state.dailyMenuOrders[menu.id] = (state.dailyMenuOrders[menu.id] || 0) + 1;
      changeMenuPopularity(menu, 1, "注文が積み重なりました。");
    });
    setCycle("sales");
    pushLog(`${settledMenus.map((menu) => menu.name).join(" / ")} の売上 +${yen(totalSales)}を計上しました。`, "売上");
    table.salesCustomerTypeId = null;
  });
}

function cycleLabel(step) {
  return TEXT.phase[step] || "";
}

function updateCycleHighlights() {
  DOM.cycleList.querySelectorAll("li").forEach((item) => {
    item.classList.toggle("active", item.dataset.step === state.cycleStep);
  });
}

function getTableStatusDisplayText(table) {
  const statusText = table?.statusText || "";
  if (statusText.includes(TEXT.cooking) || statusText.includes(TEXT.waitServe)) {
    return "料理待ち";
  }
  return statusText;
}

function getCustomerTableOffsetX(customer) {
  // ここで客画像の横幅差による表示位置を補正します。画像の中央がテーブル中央に来るようにします。
  const customerWidthMap = {
    student: 59,
    "office-worker": 61,
    otaku: 67,
    couple: 135,
  };
  const tableCustomerAnchorWidth = 64;
  const customerWidth = customerWidthMap[customer?.customerType?.id] || tableCustomerAnchorWidth;
  return Math.round((tableCustomerAnchorWidth - customerWidth) / 2);
}

function renderTables() {
  DOM.tables.innerHTML = "";

  getActiveTables().forEach((table) => {
    const tableEl = document.createElement("article");
    tableEl.className = "table";
    tableEl.style.left = `${table.x}px`;
    tableEl.style.top = `${table.y}px`;

    // ここで席ごとの客画像の表示座標を使用します。
    const customerHtml = table.occupant
        ? `<div class="customer" style="left:${table.customerX + getCustomerTableOffsetX(table.occupant)}px; top:${table.customerY}px;"><img src="${table.occupant.avatarImage}" alt="${table.occupant.avatar}"></div>`
        : "";
    // ここで席ごとの料理画像の表示座標を使用します。
    const mealPositions = getMealPositionClasses(table.visualMeals);
    const mealHtml = table.visualMeals.length
        ? `<div class="meal-stack" style="left:${table.mealX}px; top:${table.mealY}px;">${table.visualMeals
            .map((mealVisual, index) => {
              const mealIcon = typeof mealVisual === "string" ? mealVisual : mealVisual.icon;
              const mealScale = typeof mealVisual === "string" ? 0.56 : mealVisual.scale ?? 0.56;
              const mealPositionClass = mealPositions[index] || "meal-center";
              return `<div class="meal ${mealPositionClass}"><img src="${mealIcon}" alt="meal" style="transform: scale(${mealScale}); transform-origin: center center;"></div>`;
            })
            .join("")}</div>`
        : "";
    // ここで席ごとの空皿画像の表示座標を使用します。
    const dirtyHtml = table.visualDish
        ? `<div class="dirty-dish" style="left:${table.dishX}px; top:${table.dishY}px;"><img src="${ASSETS.foods.emptyPlate}" alt="empty plate"></div>`
        : "";

    tableEl.innerHTML = `
      <div class="table-top"></div>
      ${customerHtml}
      ${mealHtml}
      ${dirtyHtml}
      <div class="table-status">${getTableStatusDisplayText(table)}</div>
    `;

    DOM.tables.appendChild(tableEl);
  });
}

function renderMaids() {
  DOM.maidsLayer.innerHTML = "";

  state.maids.forEach((maid) => {
    const isResearching = maid.task?.type === "researchRecipe";
    const bubbleText = isResearching ? "レシピ開発中" : maid.bubbleText;
    const maidEl = document.createElement("div");
    maidEl.className = `maid ${maid.variantClass}`.trim();
    maidEl.style.left = `${maid.x}px`;
    maidEl.style.top = `${maid.y}px`;
    // ここで調理中のメイドだけ、ほかのメイドより後ろのレイヤーに配置します。
    maidEl.style.zIndex = maid.task?.type === "cookMeal" ? "1" : "3";
    maidEl.innerHTML = `
      <div class="maid-bubble">${bubbleText}</div>
      <div class="maid-sprite">
        <img src="${ASSETS.maids[maid.id]}" alt="${maidLabel(maid)}">
      </div>
      <div class="maid-name">${maidLabel(maid)}</div>
    `;
    DOM.maidsLayer.appendChild(maidEl);
  });
}

function renderLog() {
  DOM.log.innerHTML = "";
  state.logEntries.forEach((entry) => {
    const div = document.createElement("div");
    div.className = "log-entry";
    div.innerHTML = entry.strong ? `<strong>${entry.strong}</strong> ${entry.message}` : entry.message;
    DOM.log.appendChild(div);
  });
}

function renderIngredientButtons() {
  DOM.ingredientGrid.innerHTML = "";
  getUnlockedIngredients().forEach((ingredient) => {
    const owned = state.inventory[ingredient.id] || 0;
    const button = document.createElement("button");
    button.type = "button";
    button.className = "ingredient-chip";
    button.disabled = owned <= 0;
    button.classList.toggle("active", state.selectedIngredients.includes(ingredient.id));
    button.innerHTML = `
      <span class="ingredient-chip-main">
        <img class="ingredient-chip-icon" src="${getIngredientIcon(ingredient.id)}" alt="${ingredient.name}">
        <span>${ingredient.name}</span>
      </span>
      <span class="ingredient-count">${owned}</span>
    `;
    button.addEventListener("click", () => {
      toggleIngredient(ingredient.id);
    });
    DOM.ingredientGrid.appendChild(button);
  });
}

function renderRecipeMaidOptions() {
  DOM.maidSelector.innerHTML = "";
  state.maids.forEach((maid) => {
    const isBusyResearching = maid.task?.type === "researchRecipe";
    const isBusyWithOtherTask = maid.task && maid.task.type !== "researchRecipe";
    const button = document.createElement("button");
    button.type = "button";
    button.className = "maid-option";
    button.classList.toggle("active", state.selectedResearchMaidId === maid.id);
    button.disabled = Boolean(isBusyWithOtherTask || isBusyResearching);
    button.innerHTML = `
      <span class="maid-option-portrait">
        <img src="${ASSETS.maids[maid.id]}" alt="${maidLabel(maid)}">
      </span>
      <span class="maid-option-mini-name">${maidLabel(maid)}${isBusyResearching ? " (開発中)" : isBusyWithOtherTask ? " (対応中)" : ""}</span>
    `;
    button.addEventListener("click", () => {
      state.selectedResearchMaidId = maid.id;
      renderRecipeDevelopment();
      renderMaids();
    });
    DOM.maidSelector.appendChild(button);
  });

  const selectedMaid = getMaidById(state.selectedResearchMaidId);
  const researchTask = selectedMaid ? getResearchTask(selectedMaid.id) : null;
  if (researchTask && selectedMaid) {
    DOM.selectedMaid.textContent = `担当: ${maidLabel(selectedMaid)} / 開発完了予定 ${formatTime(researchTask.endsAt)}`;
  } else {
    DOM.selectedMaid.textContent = selectedMaid ? `担当: ${maidLabel(selectedMaid)}` : "担当: 未選択";
  }
}

function toggleIngredient(ingredientId) {
  const selectedMaid = getMaidById(state.selectedResearchMaidId);
  if (selectedMaid?.task?.type === "researchRecipe") {
    return;
  }
  if ((state.inventory[ingredientId] || 0) <= 0) {
    return;
  }
  if (state.selectedIngredients.includes(ingredientId)) {
    state.selectedIngredients = state.selectedIngredients.filter((id) => id !== ingredientId);
  } else {
    state.selectedIngredients = [...state.selectedIngredients, ingredientId].slice(-6);
  }
  renderRecipeDevelopment();
}

function developRecipe() {
  const selectedMaid = getMaidById(state.selectedResearchMaidId);
  if (!selectedMaid) {
    state.recipeResult = "担当するメイドを選んでください。";
    state.recipeResultImage = "";
    renderRecipeDevelopment();
    return;
  }

  if (selectedMaid.task?.type === "researchRecipe") {
    state.recipeResult = `${maidLabel(selectedMaid)}は現在レシピ開発中です。完了までお待ちください。`;
    renderRecipeDevelopment();
    return;
  }

  if (selectedMaid.task) {
    state.recipeResult = `${maidLabel(selectedMaid)}は接客対応中のため、今はレシピ開発に入れません。`;
    renderRecipeDevelopment();
    return;
  }

  if (state.selectedIngredients.length < 2) {
    state.recipeResult = "食材を2つ以上選んでから開発してください。";
    state.recipeResultImage = "";
    renderRecipeDevelopment();
    return;
  }

  const selectedKey = normalizedIngredientKey(state.selectedIngredients);
  const matchedRecipe = RECIPE_DEVELOPMENT.find(
    (recipe) => normalizedIngredientKey(recipe.ingredients) === selectedKey
  );

  // すでに開発済みの組み合わせは、その場で案内して食材を消費しません。
  if (matchedRecipe) {
    const developedMenu = getMenuById(matchedRecipe.menuId);
    if (developedMenu?.unlocked) {
      state.recipeResult = `${developedMenu.name} はすでに開発済みです。別の組み合わせを試してください。`;
      state.recipeResultImage = developedMenu.icon || getMenuIcon(developedMenu.id);
      renderRecipeDevelopment();
      return;
    }
  }

  clearRecipeResultDisplay();
  const usedIngredients = [...state.selectedIngredients];
  consumeSelectedIngredients(usedIngredients);
  const duration = getResearchTaskDuration(selectedMaid, usedIngredients.length);
  selectedMaid.task = {
    type: "researchRecipe",
    startedAt: state.currentMinute,
    endsAt: state.currentMinute + duration,
    bubble: "レシピ開発中",
    cycle: "research",
    usedIngredients,
    matchedRecipe,
  };
  selectedMaid.bubbleText = "レシピ開発中";
  moveMaid(selectedMaid, selectedMaid.homeX, selectedMaid.homeY);
  state.recipeResult = `${maidLabel(selectedMaid)}がレシピ開発を開始しました。完了予定は ${formatTime(selectedMaid.task.endsAt)} です。`;
  state.recipeResultImage = "";
  pushLog(`${maidLabel(selectedMaid)}がレシピ開発を開始しました。完了予定 ${formatTime(selectedMaid.task.endsAt)} / 使用食材: ${usedIngredients.map((id) => getIngredientById(id)?.name || id).join(" / ")}`, "開発開始");
  state.selectedIngredients = [];
  renderRecipeDevelopment();
  renderMaids();
  renderLog();
}

function renderRecipeDevelopment() {
  renderRecipeMaidOptions();
  renderIngredientButtons();
  const selectedMaid = getMaidById(state.selectedResearchMaidId);
  const activeResearch = selectedMaid ? getResearchTask(selectedMaid.id) : null;
  const selectedNames = state.selectedIngredients
    .map((id) => INGREDIENTS.find((ingredient) => ingredient.id === id)?.name)
    .filter(Boolean);
  DOM.selectedIngredients.innerHTML =
    selectedNames.length > 0
      ? state.selectedIngredients
          .map((id) => {
            const ingredient = getIngredientById(id);
            return ingredient
              ? `<span class="selected-ingredient-tag"><img src="${getIngredientIcon(id)}" alt="${ingredient.name}"><span>${ingredient.name}</span></span>`
              : "";
          })
          .join("")
      : `<span class="selected-ingredient-empty">選択中: なし</span>`;
  DOM.recipeResult.textContent = state.recipeResult;
  DOM.recipeResultVisual.innerHTML = state.recipeResultImage
    ? `<img src="${state.recipeResultImage}" alt="recipe result">`
    : "ここに開発成功したメニュー画像が表示されます";
  if (DOM.developBtn) {
    DOM.developBtn.disabled = Boolean(activeResearch || selectedMaid?.task || state.selectedIngredients.length < 2);
    DOM.developBtn.textContent = activeResearch ? `開発中 (${formatTime(activeResearch.endsAt)} 完了予定)` : "レシピ開発を実行";
  }
}

function renderRecipeBook() {
  ensureInitialMenusAvailable();
  const recipeBookMenus = getRecipeBookMenus();
  const canTogglePublication = state.systemUnlocks.menuPublication;
  if (recipeBookMenus.length === 0) {
    DOM.recipeBookList.innerHTML = `<div class="recipe-book-item"><div class="recipe-book-title">まだ表示できるレシピがありません。</div></div>`;
    return;
  }

  const popularityRanks = new Map(
    [...recipeBookMenus]
      .sort((a, b) => {
        const popularityDiff = (b.popularity ?? 0) - (a.popularity ?? 0);
        if (popularityDiff !== 0) {
          return popularityDiff;
        }
        return a.name.localeCompare(b.name, "ja");
      })
      .map((menu, index) => [menu.id, index + 1])
  );

  DOM.recipeBookList.innerHTML = recipeBookMenus
    .map((menu) => {
      const icon = menu.icon || getMenuIcon(menu.id);
      const genreLabel = getGenreLabel(menu.genre || "drink");
      const phaseLabel = getPhaseLabel(menu.phase || "initial");
      const statusText = menu.isPublished ? "販売中" : "販売停止";
      const statusClass = menu.isPublished ? "is-published" : "is-hidden";
      const popularityRank = popularityRanks.get(menu.id);
      const publishButtonHtml = canTogglePublication
        ? `<button type="button" class="recipe-publish-btn ${statusClass}" data-menu-id="${menu.id}">
                  ${statusText}
                </button>`
        : "";

      return `
        <article class="recipe-book-item" data-menu-id="${menu.id}">
          <div class="recipe-book-hero">
            <div class="recipe-book-icon"><img src="${icon}" alt="${menu.name}" onerror="this.src='assets/foods/unknown_food.png'"></div>
            <div class="recipe-book-summary">
              <div class="recipe-book-head">
                <div class="recipe-book-title-wrap">
                  <div>
                    <div class="recipe-book-title">${menu.name}</div>
                    <div class="recipe-book-genre">${genreLabel} / ${phaseLabel}</div>
                  </div>
                </div>
                ${publishButtonHtml}
              </div>
              <div class="recipe-book-metrics">
                <span class="recipe-book-chip">目安評判 ${menu.recommendedReputation ?? 0}</span>
                <span class="recipe-book-chip">価格${yen(menu.price ?? 0)}</span>
                <span class="recipe-book-chip">原価 ${yen(getMenuCost(menu))}</span>
                <span class="recipe-book-chip">利益率 ${getMenuMarginRate(menu)}%</span>
                <span class="recipe-book-chip">調理 ${menu.cookTime ?? 0}分</span>
                <span class="recipe-book-chip">人気順位 ${popularityRank ? `${popularityRank}位` : "-"}</span>
              </div>
            </div>
          </div>
          <div class="recipe-book-section">
            <div class="recipe-book-section-title">必要食材</div>
            <div class="recipe-book-ingredient-list">${renderMenuIngredientTags(menu)}</div>
          </div>
        </article>
      `;
    })
    .join("");

  DOM.recipeBookList.querySelectorAll(".recipe-publish-btn").forEach((button) => {
    button.addEventListener("click", () => {
      toggleRecipePublication(button.dataset.menuId);
    });
  });
}

function toggleRecipePublication(menuId) {
  if (!state.systemUnlocks.menuPublication) {
    pushLog("販売中・販売停止の切り替えは、第14章以降に解放されます。", "メニュー");
    renderLog();
    return;
  }
  const menu = getMenuById(menuId);
  if (!menu || (!menu.unlocked && !isInitialMenu(menu.id))) {
    return;
  }

  const publishedMenus = getRecipeBookMenus().filter((recipe) => recipe.isPublished);
  if (menu.isPublished && publishedMenus.length === 1) {
    pushLog("最低1品は販売中にしておく必要があります。", "メニュー");
    renderLog();
    return;
  }

  menu.isPublished = !menu.isPublished;
  pushLog(
    `${menu.name}を${menu.isPublished ? "販売中" : "販売停止"}に設定しました。`,
    "メニュー"
  );
  renderRecipeBook();
  updateUI();
}

function renderMaidList() {
  DOM.maidList.innerHTML = "";
  state.maids.forEach((maid) => {
    const serviceThreshold = getMaidExpThreshold(maid, "service");
    const cookingThreshold = getMaidExpThreshold(maid, "cooking");
    const researchThreshold = getMaidExpThreshold(maid, "research");
    const item = document.createElement("div");
    item.className = "maid-list-item";
    item.innerHTML = `
      <div class="maid-list-title-row">
        <div class="maid-list-hero">
          <div class="maid-list-portrait"><img src="${ASSETS.maids[maid.id]}" alt="${maidLabel(maid)}"></div>
          <div class="maid-list-title-block">
            <div class="maid-list-title">${maidLabel(maid)}</div>
              <div class="maid-list-flavor">${maid.traits?.flavorText || "バランスよく動けます。"}</div>
          </div>
        </div>
      </div>
        <div class="maid-list-stats">
          <div>接客: ${maid.stats.service} / ${MAX_MAID_STAT}</div>
          <div>調理: ${maid.stats.cooking} / ${MAX_MAID_STAT}</div>
          <div>レシピ開発: ${maid.stats.research} / ${MAX_MAID_STAT}</div>
          <div>好感度: ${state.affection[maid.id] || 0}</div>
          <div>EXP: 接客 ${maid.exp.service}${serviceThreshold ? `/${serviceThreshold}` : "/MAX"} / 調理 ${maid.exp.cooking}${cookingThreshold ? `/${cookingThreshold}` : "/MAX"} / 開発 ${maid.exp.research}${researchThreshold ? `/${researchThreshold}` : "/MAX"}</div>
        </div>
      `;
      DOM.maidList.appendChild(item);
  });
}

function getAvailableAdvScenes() {
  return ADV_SCENES.filter((scene) => {
    if (!isAdvSceneUnlocked(scene)) {
      return false;
    }
    return !(isMainAdvSceneId(scene.id) && state.completedAdvSceneIds.includes(scene.id));
  });
}

function getDebugAdvScenes() {
  return ADV_SCENES;
}

function isMainAdvSceneId(sceneId) {
  return (ADV_SCENARIOS.main || []).some((scene) => scene.id === sceneId);
}

function hasCompletedAnyAdvScene(sceneIds) {
  return sceneIds.some((sceneId) => state.completedAdvSceneIds.includes(sceneId));
}

function hasSeenAnyAdvScene(sceneIds) {
  return sceneIds.some((sceneId) => {
    return (
      state.completedAdvSceneIds.includes(sceneId) ||
      state.unlockedAdvSceneIds.includes(sceneId) ||
      state.activeAdvSceneId === sceneId
    );
  });
}

function getCompletedMainChapterNumber() {
  const chapterChecks = [
    { chapter: 5, ids: ["main-promotion-results-poster", "main-promotion-results-flyer", "main-promotion-results-sns"] },
    { chapter: 4, ids: ["main-first-turning-point", "main-first-turning-point-poster", "main-first-turning-point-flyer", "main-first-turning-point-sns"] },
    { chapter: 3, ids: ["main-grandfather-memory"] },
    { chapter: 2, ids: ["main-chapter-2-delivery"] },
    { chapter: 1, ids: ["main-first-arrival"] },
  ];
  const matched = chapterChecks.find((entry) => hasCompletedAnyAdvScene(entry.ids));
  return matched?.chapter || 0;
}

function getNightTalkPhase() {
  const chapterFiveCompleted = hasCompletedAnyAdvScene([
    "main-promotion-results-poster",
    "main-promotion-results-flyer",
    "main-promotion-results-sns",
  ]);
  if (chapterFiveCompleted) {
    return {
      id: "growth",
      prompt: "まだ余裕はないけど、少しずつ分かってきたし、今日も誰かと話しておくか。",
    };
  }
  return {
    id: "early",
    prompt: "数字は厳しいけど、それでも今日は誰かと少し話しておきたいな……",
  };
}

function getNightTalkPoolKey(phaseId, maidId) {
  return `${phaseId}:${maidId}`;
}

function pickNightTalkIndex(phaseId, maidId) {
  const pool = window.MADE_CAFE_NIGHT_TALKS?.[phaseId]?.[maidId] || [];
  if (pool.length === 0) {
    return -1;
  }
  const historyKey = getNightTalkPoolKey(phaseId, maidId);
  const seenEntries = state.nightTalkHistory[historyKey] || [];
  const unseenIndices = pool.map((_, index) => index).filter((index) => !seenEntries.includes(index));

  if (unseenIndices.length === 0) {
    state.nightTalkHistory[historyKey] = [];
    return Math.floor(Math.random() * pool.length);
  }

  return unseenIndices[Math.floor(Math.random() * unseenIndices.length)];
}

function rememberNightTalkIndex(phaseId, maidId, talkIndex) {
  if (talkIndex < 0) {
    return;
  }
  const historyKey = getNightTalkPoolKey(phaseId, maidId);
  const seenEntries = state.nightTalkHistory[historyKey] || [];
  if (!seenEntries.includes(talkIndex)) {
    seenEntries.push(talkIndex);
    state.nightTalkHistory[historyKey] = seenEntries;
  }
}

function getNightTalkEntry(phaseId, maidId, talkIndex) {
  const pool = window.MADE_CAFE_NIGHT_TALKS?.[phaseId]?.[maidId] || [];
  return pool[talkIndex] || null;
}

function getNightTalkLines(phaseId, maidId, talkIndex) {
  const talk = getNightTalkEntry(phaseId, maidId, talkIndex);
  if (!talk) {
    return [
      { speakerId: maidId, expression: "default", text: "……今日は少しだけ、落ち着いて話せましたね。" },
      { speakerName: "主人公", text: "ああ。こういう時間も大事だな。" },
    ];
  }

  if (!Array.isArray(talk) && Array.isArray(talk.lines)) {
    return talk.lines;
  }

  if (typeof talk[0] === "object" && !Array.isArray(talk[0])) {
    return talk;
  }

  const maid = getMaidById(maidId);
  return talk.map((text, index) => {
    if (index % 2 === 0) {
      return { speakerId: maidId, expression: maid?.expression || "default", text };
    }
    return { speakerName: "主人公", text };
  });
}

const CHARACTER_ROUTE_SCENES = {
  "maid-1": [],
  "maid-2": [],
  "maid-3": [],
};

// ここで個別シナリオの好感度条件を設定します。
// 数値はあとで見直しやすいよう、ここにまとめています。
const CHARACTER_ROUTE_AFFECTION_THRESHOLDS = {
  route1: 8,
  route2: 16,
  route3: 24,
  route4: 32,
  ending: 50,
};

const FINAL_ENDING_SCENE_IDS = [
  "ending-bad",
];

function isFinalEndingScene(sceneId) {
  return FINAL_ENDING_SCENE_IDS.includes(sceneId);
}

function getCharacterRouteWindows() {
  return [];
}

function getCharacterRouteWindow() {
  return getCharacterRouteWindows()[0] || null;
}

function getCharacterRouteSceneId(maidId) {
  const routeWindows = getCharacterRouteWindows();
  if (routeWindows.length === 0) {
    return null;
  }

  const affection = state.affection[maidId] || 0;
  for (const windowInfo of routeWindows) {
    if (affection < windowInfo.threshold) {
      continue;
    }

    const sceneId = CHARACTER_ROUTE_SCENES[maidId]?.[windowInfo.routeIndex] || null;
    if (!sceneId) {
      continue;
    }
    if (!ADV_SCENES.some((scene) => scene.id === sceneId)) {
      console.warn(`個別イベントが登録されていません: ${sceneId}`);
      continue;
    }

    // ここで個別シナリオを順番通りに進めるため、ひとつ前の個別シナリオを見ているか確認します。
    if (windowInfo.routeIndex > 0) {
      const previousSceneId = CHARACTER_ROUTE_SCENES[maidId]?.[windowInfo.routeIndex - 1] || null;
      if (previousSceneId && !state.completedAdvSceneIds.includes(previousSceneId)) {
        continue;
      }
    }

    if (state.completedAdvSceneIds.includes(sceneId)) {
      continue;
    }

    return sceneId;
  }

  return null;
}

function getNightTalkBackgroundId() {
  return "cafeHallNight";
}

function getNightTalkBranchSceneId(day, maidId) {
  return `night-talk-${day}-${maidId}`;
}

function getNightTalkChoiceNextSceneId(day, maidId) {
  return getCharacterRouteSceneId(maidId) || getNightTalkBranchSceneId(day, maidId);
}

function buildNightTalkIntroScene(day) {
  const phase = getNightTalkPhase();
  if (!phase) {
    return null;
  }

  return {
    id: `night-talk-${day}`,
    title: "夜の会話",
    summary: "日報のあと、誰かと少しだけ話す。",
    unlock: () => true,
    background: getNightTalkBackgroundId(),
    lines: [
      { speakerName: "主人公", text: phase.prompt },
      {
        text: "……誰と話そうか。",
        choices: [
          { label: "ことね", value: "maid-1", nextSceneId: getNightTalkChoiceNextSceneId(day, "maid-1") },
          { label: "すず", value: "maid-2", nextSceneId: getNightTalkChoiceNextSceneId(day, "maid-2") },
          { label: "ひな", value: "maid-3", nextSceneId: getNightTalkChoiceNextSceneId(day, "maid-3") },
        ],
      },
    ],
  };
}

function buildNightTalkBranchScene(day, maidId) {
  const phase = getNightTalkPhase();
  const maid = getMaidById(maidId);
  if (!phase || !maid) {
    return null;
  }
  const selectionKey = `night-talk-${day}-${maidId}`;
  let talkIndex = state.nightTalkSelections[selectionKey];
  if (typeof talkIndex !== "number") {
    talkIndex = pickNightTalkIndex(phase.id, maidId);
    rememberNightTalkIndex(phase.id, maidId, talkIndex);
    state.nightTalkSelections[selectionKey] = talkIndex;
  }
  const talkEntry = getNightTalkEntry(phase.id, maidId, talkIndex);
  if (talkEntry && !Array.isArray(talkEntry) && Array.isArray(talkEntry.choices)) {
    return {
      id: selectionKey,
      title: `${maidLabel(maid)}との夜会話`,
      summary: `${maidLabel(maid)}と、その日の終わりに少しだけ話す。`,
      unlock: () => true,
      background: talkEntry.background || getNightTalkBackgroundId(),
      lines: [
        ...getNightTalkLines(phase.id, maidId, talkIndex),
        {
          text: "……どう答える？",
          choices: talkEntry.choices.map((choice, index) => ({
            label: choice.label,
            value: `${maidId}:${talkIndex}:${index}`,
            maidId,
            affectionDelta: choice.affectionDelta || 0,
            affectionReason: choice.affectionReason || `${maidLabel(maid)}との夜会話で「${choice.label}」と答えた。`,
            nextSceneId: `night-talk-${day}-${maidId}-choice-${talkIndex}-${index}`,
          })),
        },
      ],
    };
  }

  return {
    id: selectionKey,
    title: `${maidLabel(maid)}との夜会話`,
    summary: `${maidLabel(maid)}と、その日の終わりに少しだけ話す。`,
    unlock: () => true,
    background: talkEntry?.background || getNightTalkBackgroundId(),
    lines: getNightTalkLines(phase.id, maidId, talkIndex),
  };
}

function buildNightTalkChoiceScene(day, maidId, talkIndex, choiceIndex) {
  const phase = getNightTalkPhase();
  const maid = getMaidById(maidId);
  const talkEntry = phase ? getNightTalkEntry(phase.id, maidId, talkIndex) : null;
  const choice = talkEntry?.choices?.[choiceIndex];
  if (!phase || !maid || !choice) {
    return null;
  }

  const responseLines = Array.isArray(choice.responseLines) && choice.responseLines.length > 0
    ? choice.responseLines
    : [
        { speakerName: "主人公", text: choice.playerText || choice.label },
        { speakerId: maidId, expression: choice.expression || "default", text: choice.response || "……そう答えるしかなかった。" },
      ];

  return {
    id: `night-talk-${day}-${maidId}-choice-${talkIndex}-${choiceIndex}`,
    title: `${maidLabel(maid)}との夜会話`,
    summary: `${maidLabel(maid)}に返事をする。`,
    unlock: () => true,
    background: talkEntry?.background || getNightTalkBackgroundId(),
    lines: responseLines,
  };
}

function getNightTalkSceneById(sceneId) {
  if (!sceneId?.startsWith("night-talk-")) {
    return null;
  }

  const choiceMatch = sceneId.match(/^night-talk-(\d+)-(maid-\d+)-choice-(\d+)-(\d+)$/);
  if (choiceMatch) {
    return buildNightTalkChoiceScene(
      Number(choiceMatch[1]),
      choiceMatch[2],
      Number(choiceMatch[3]),
      Number(choiceMatch[4])
    );
  }

  const branchMatch = sceneId.match(/^night-talk-(\d+)-(maid-\d+)$/);
  if (branchMatch) {
    return buildNightTalkBranchScene(Number(branchMatch[1]), branchMatch[2]);
  }

  const introMatch = sceneId.match(/^night-talk-(\d+)$/);
  if (introMatch) {
    return buildNightTalkIntroScene(Number(introMatch[1]));
  }

  return null;
}

function triggerNightTalkScene(options = {}) {
  const scene = buildNightTalkIntroScene(state.day);
  if (!scene || state.completedAdvSceneIds.includes(scene.id)) {
    return false;
  }
  return openAdvSceneById(scene.id, options);
}

function getAdvSceneById(sceneId) {
  return ADV_SCENES.find((scene) => scene.id === sceneId) || getNightTalkSceneById(sceneId) || null;
}

function isAdvInlineResponseActive() {
  return Array.isArray(state.activeAdvInlineLines) && state.activeAdvInlineLines.length > 0;
}

function clearAdvInlineResponse() {
  state.activeAdvInlineLines = [];
  state.activeAdvInlineLineIndex = 0;
  state.activeAdvInlineReturnIndex = null;
}

function getCurrentAdvLine() {
  const scene = getAdvSceneById(state.activeAdvSceneId);
  if (!scene) {
    return null;
  }
  if (isAdvInlineResponseActive()) {
    return state.activeAdvInlineLines[state.activeAdvInlineLineIndex] || null;
  }
  return scene.lines[state.activeAdvLineIndex] || null;
}

function getAdvLineIndexByLabel(scene, label) {
  if (!scene || !label) {
    return -1;
  }
  return scene.lines.findIndex((line) => line?.label === label);
}

function getAdvLineBackgroundId(scene, line) {
  // 背景指定は「次に背景を指定する行」まで維持します。
  // 例: 店前 -> 店内のように途中で場面転換したあと、以降の会話も同じ背景で表示します。
  if (!scene || !Array.isArray(scene.lines)) {
    return line?.background || scene?.background || "cafeHallDay";
  }
  const lineIndex = scene.lines.indexOf(line);
  if (lineIndex >= 0) {
    for (let index = lineIndex; index >= 0; index -= 1) {
      const background = scene.lines[index]?.background;
      if (background) {
        return background;
      }
    }
  }
  return scene?.background || "cafeHallDay";
}

function isAdvSceneUnlocked(scene) {
  const condition = scene?.unlock;
  if (!condition) {
    return true;
  }
  if (typeof condition === "function") {
    try {
      return condition();
    } catch (error) {
      return false;
    }
  }
  if (condition.dayGte != null && state.day < condition.dayGte) {
    return false;
  }
  if (condition.reputationGte != null && state.reputation < condition.reputationGte) {
    return false;
  }
  if (condition.unlockedMenusGte != null && getUnlockedMenus().length < condition.unlockedMenusGte) {
    return false;
  }
  if (condition.sceneIdUnlocked && !state.unlockedAdvSceneIds.includes(condition.sceneIdUnlocked)) {
    return false;
  }
  return true;
}

function clearAdvTransitionTimer() {
  if (state.advTransitionTimer) {
    clearTimeout(state.advTransitionTimer);
    state.advTransitionTimer = null;
  }
}

function clearAdvCgIntroTimer(options = {}) {
  if (state.advCgIntroTimer) {
    clearTimeout(state.advCgIntroTimer);
    state.advCgIntroTimer = null;
  }
  state.isAdvCgIntro = false;
  DOM.advStage?.classList.remove("is-cg-intro");
  DOM.advStage?.classList.remove("is-cg-prelude");
  if (!options.preserveBlackTransition) {
    DOM.advStage?.classList.remove("is-black-transition");
  }
}

function startAdvCgIntro() {
  clearAdvAutoTimer();
  clearAdvCgIntroTimer();
  state.isAdvCgIntro = true;
  DOM.advStage?.classList.add("is-cg-intro");
  state.advCgIntroTimer = setTimeout(() => {
    state.advCgIntroTimer = null;
    state.isAdvCgIntro = false;
    DOM.advStage?.classList.remove("is-cg-intro");
    queueAdvAutoAdvance();
  }, 2000);
}

function clearAdvAutoTimer() {
  if (state.advAutoTimer) {
    clearTimeout(state.advAutoTimer);
    state.advAutoTimer = null;
  }
}

function clearAdvLineAutoTimer() {
  if (state.advLineAutoTimer) {
    clearTimeout(state.advLineAutoTimer);
    state.advLineAutoTimer = null;
  }
  state.isAdvLineAutoWaiting = false;
}

function queueAdvLineAutoAdvance(line) {
  clearAdvLineAutoTimer();
  const waitMs = Number(line?.autoAdvanceMs || 0);
  if (!waitMs || waitMs <= 0) {
    return;
  }
  clearAdvAutoTimer();
  state.isAdvLineAutoWaiting = true;
  state.advLineAutoTimer = setTimeout(() => {
    state.advLineAutoTimer = null;
    state.isAdvLineAutoWaiting = false;
    if (state.isAdvModalOpen && !state.isAdvTransitioning && !state.isAdvCgIntro) {
      nextAdvLine();
    }
  }, waitMs);
}

function queueAdvAutoAdvance() {
  clearAdvAutoTimer();
  if (!state.isAdvAutoMode || !state.isAdvModalOpen || state.isAdvTransitioning || state.isAdvCgIntro || state.isAdvLineAutoWaiting) {
    return;
  }
  const line = getCurrentAdvLine();
  if (!line || line.autoAdvanceMs || (Array.isArray(line.choices) && line.choices.length > 0)) {
    return;
  }
  state.advAutoTimer = setTimeout(() => {
    state.advAutoTimer = null;
    if (state.isAdvAutoMode && state.isAdvModalOpen && !state.isAdvTransitioning) {
      nextAdvLine();
    }
  }, 1700);
}

function updateAdvAutoButton() {
  if (!DOM.advAutoBtn) {
    return;
  }
  DOM.advAutoBtn.classList.toggle("is-active", state.isAdvAutoMode);
  DOM.advAutoBtn.textContent = state.isAdvAutoMode ? "オート中" : "オート";
}

function toggleAdvAutoMode() {
  state.isAdvAutoMode = !state.isAdvAutoMode;
  updateAdvAutoButton();
  if (state.isAdvAutoMode) {
    queueAdvAutoAdvance();
  } else {
    clearAdvAutoTimer();
  }
}

function transitionAdvScene(callback, options = {}) {
  clearAdvTransitionTimer();
  state.isAdvTransitioning = true;
  const useBlackTransition = options.fadeTo === "black" || options.fadeFrom === "black";
  DOM.advStage?.classList.toggle("is-black-transition", useBlackTransition);
  DOM.advStage.classList.add("is-faded");
  state.advTransitionTimer = setTimeout(() => {
    callback();
    requestAnimationFrame(() => {
      DOM.advStage.classList.remove("is-faded");
      state.advTransitionTimer = setTimeout(() => {
        state.isAdvTransitioning = false;
        DOM.advStage?.classList.remove("is-black-transition");
        state.advTransitionTimer = null;
      }, 240);
    });
  }, 220);
}

function renderAdvSceneList() {
  if (!DOM.advSceneList) {
    return;
  }
  const scenes = state.isAdvDebugMode ? getDebugAdvScenes() : getAvailableAdvScenes();
  DOM.advSceneList.innerHTML = "";

  if (scenes.length === 0) {
    DOM.advSceneList.innerHTML = `<div class="daily-report-summary">${state.isAdvDebugMode ? "ADVシナリオが登録されていません。" : "まだ解放されている会話イベントはありません。"}</div>`;
    return;
  }

  scenes.forEach((scene) => {
    const button = document.createElement("button");
    const isCompleted = state.completedAdvSceneIds.includes(scene.id);
    button.type = "button";
    button.className = "adv-scene-item";
    button.innerHTML = `
      <div class="adv-scene-item-title">
        <span>${scene.title}</span>
        ${state.isAdvDebugMode ? `<span class="adv-scene-badge">DEBUG</span>` : isCompleted ? "" : `<span class="adv-scene-badge">NEW</span>`}
      </div>
      <div class="adv-scene-item-summary">${scene.summary || scene.id}</div>
    `;
    button.classList.toggle("active", state.activeAdvSceneId === scene.id);
    button.addEventListener("click", () => startAdvScene(scene.id));
    DOM.advSceneList.appendChild(button);
  });
}

function renderAdvScene() {
  clearAdvAutoTimer();
  clearAdvLineAutoTimer();
  const scene = getAdvSceneById(state.activeAdvSceneId);
  if (!scene) {
    DOM.advSceneTitle.textContent = "会話イベントを選択してください";
    DOM.advSpeaker.textContent = "";
    DOM.advText.textContent = "日数や評判、開発したメニューに応じて会話イベントが増えていきます。";
    DOM.advTextbox?.classList.remove("is-hidden-by-line");
    DOM.advBackground.style.backgroundImage = `linear-gradient(180deg, rgba(22, 18, 28, 0.04), rgba(22, 18, 28, 0.18)), url("${getAdvBackgroundAsset("black")}")`;
    renderAdvEventCg(null, null);
    renderAdvPortraitSlots(null);
    DOM.advChoices.innerHTML = "";
    DOM.advContinue.classList.remove("is-visible");
    DOM.advNextBtn.disabled = true;
    if (DOM.advSkipBtn) {
      DOM.advSkipBtn.disabled = true;
    }
    updateAdvAutoButton();
    return;
  }

  const line = getCurrentAdvLine() || scene.lines[0];
  const speaker = line ? getMaidById(line.speakerId) : null;
  const speakerName = line?.speakerName || (speaker ? maidLabel(speaker) : "");
  const backgroundId = getAdvLineBackgroundId(scene, line);
  const backgroundAsset = getAdvBackgroundAsset(backgroundId);
  prepareAdvCgPrelude(scene, line);
  // 背景・立ち絵・本文をまとめて切り替えて、ADVの1行を描画します。
  DOM.advBackground.style.backgroundImage = `linear-gradient(180deg, rgba(22, 18, 28, 0.04), rgba(22, 18, 28, 0.18)), url("${backgroundAsset}"), url("${getAdvBackgroundAsset("black")}")`;
  DOM.advSceneTitle.textContent = scene.title;
  DOM.advSpeaker.textContent = speakerName;
  DOM.advText.textContent = line?.text || "";
  DOM.advTextbox?.classList.toggle("is-hidden-by-line", Boolean(line?.hideTextbox));
  renderAdvEventCg(scene, line);
  renderAdvPortraitSlots(line);
  DOM.advChoices.innerHTML = "";
  const hasChoices = Array.isArray(line?.choices) && line.choices.length > 0;
  if (hasChoices) {
    line.choices.forEach((choice, index) => {
      const button = document.createElement("button");
      button.type = "button";
      button.className = "adv-choice-btn";
      button.textContent = choice.label;
      button.addEventListener("click", () => chooseAdvOption(index));
      DOM.advChoices.appendChild(button);
    });
    DOM.advContinue.classList.remove("is-visible");
  } else {
    DOM.advContinue.classList.add("is-visible");
  }
  DOM.advNextBtn.disabled = !scene || hasChoices;
  if (DOM.advSkipBtn) {
    DOM.advSkipBtn.disabled = false;
  }
  updateAdvAutoButton();
  queueAdvLineAutoAdvance(line);
  queueAdvAutoAdvance();
}

function startAdvScene(sceneId) {
  const scene = getAdvSceneById(sceneId);
  const selectableScenes = state.isAdvDebugMode ? getDebugAdvScenes() : getAvailableAdvScenes();
  if (!scene || !selectableScenes.some((availableScene) => availableScene.id === sceneId)) {
    return;
  }

  const currentScene = getAdvSceneById(state.activeAdvSceneId);
  const currentLine = getCurrentAdvLine();
  const currentBackgroundId = getAdvLineBackgroundId(currentScene, currentLine);
  const nextBackgroundId = getAdvLineBackgroundId(scene, scene.lines[0]);
  const applySceneChange = () => {
    clearAdvCgIntroTimer();
    state.activeAdvEventCgId = "";
    state.activeAdvSceneId = sceneId;
    state.activeAdvLineIndex = 0;
    renderAdvSceneList();
    renderAdvScene();
  };

  if (currentBackgroundId !== nextBackgroundId) {
    prepareAdvCgPrelude(scene, scene.lines[0]);
    transitionAdvScene(applySceneChange, { fadeFrom: currentBackgroundId, fadeTo: nextBackgroundId });
    return;
  }

  applySceneChange();
}

function completeAdvScene(scene) {
  if (!scene) {
    closeAdvModal();
    return;
  }
  if (state.isAdvDebugMode) {
    pushLog(`${scene.title} のデバッグ再生を終了しました。`, "ADV");
    closeAdvModal();
    return;
  }
  if (!state.completedAdvSceneIds.includes(scene.id)) {
    state.completedAdvSceneIds.push(scene.id);
    pushLog(`${scene.title} を読み終えました。`, "ADV");
  }
  if (isFinalEndingScene(scene.id)) {
    showEndingThanksScreen(scene.id);
    return;
  }
  if (scene.id === "main-first-arrival") {
    scheduleScriptedInventoryOrder({ egg: 20, rice: 10, sugar: 10 }, "第1章の流れで基本食材を発注しました。");
  }
  if (scene.id === "main-chapter-2-delivery") {
    finishChapterTwoTutorial();
  }
  if (
    scene.id === "main-first-turning-point" ||
    scene.id === "main-first-turning-point-poster" ||
    scene.id === "main-first-turning-point-flyer" ||
    scene.id === "main-first-turning-point-sns"
  ) {
    finishChapterFourPromotionChoice();
  }
  if (
    scene.id === "main-promotion-results-poster" ||
    scene.id === "main-promotion-results-flyer" ||
    scene.id === "main-promotion-results-sns"
  ) {
    pushLog("第5章まで到達しました。体験版範囲の案内を表示します。", "体験版");
  }
  if (scene.id === "main-prologue-inheritance") {
    captureDayStartCheckpoint();
  }
  closeAdvModal();
}

function skipAdvScene() {
  const scene = getAdvSceneById(state.activeAdvSceneId);
  let line = getCurrentAdvLine();
  if (!scene || !line || state.isAdvTransitioning || state.isAdvCgIntro) {
    return;
  }
  clearAdvLineAutoTimer();

  if (Array.isArray(line.choices) && line.choices.length > 0) {
    return;
  }

  if (isAdvInlineResponseActive()) {
    const returnIndex = typeof state.activeAdvInlineReturnIndex === "number"
      ? state.activeAdvInlineReturnIndex
      : state.activeAdvLineIndex + 1;
    clearAdvInlineResponse();
    if (returnIndex >= scene.lines.length) {
      completeAdvScene(scene);
      return;
    }
    state.activeAdvLineIndex = returnIndex;
    line = scene.lines[state.activeAdvLineIndex] || line;
  }

  const nextChoiceIndex = scene.lines.findIndex((nextLine, index) => {
    return index > state.activeAdvLineIndex && Array.isArray(nextLine?.choices) && nextLine.choices.length > 0;
  });

  if (nextChoiceIndex >= 0) {
    const nextLine = scene.lines[nextChoiceIndex];
    const currentBackgroundId = getAdvLineBackgroundId(scene, line);
    const nextBackgroundId = getAdvLineBackgroundId(scene, nextLine);
    const applySkip = () => {
      state.activeAdvLineIndex = nextChoiceIndex;
      renderAdvScene();
    };

    if (currentBackgroundId !== nextBackgroundId) {
      prepareAdvCgPrelude(scene, nextLine);
      transitionAdvScene(applySkip, { fadeFrom: currentBackgroundId, fadeTo: nextBackgroundId });
      return;
    }

    applySkip();
    return;
  }

  // スキップでも通常読了と同じ完了処理を通し、章進行・機能解放・好感度イベントの判定を落とさないようにします。
  completeAdvScene(scene);
}

function finishChapterTwoTutorial() {
  state.systemUnlocks.inventoryManagement = true;
  state.systemUnlocks.recipeDevelopment = true;
  queueSystemNotice(
    "unlock-inventory-and-recipe",
    "在庫管理・レシピ開発が解放されました！",
    "「在庫管理」からは毎日１回だけ食材が発注できます。\n発注した食材は翌朝に届き、納品時に資金が減ります。\n\n「レシピ開発」では、食材を2つ以上選んで新メニューを開発できます。\n開発中のメイドは接客や調理に参加できません。"
  );

  const omuriceMenu = getMenuById("omurice");
  if (omuriceMenu) {
    omuriceMenu.unlocked = true;
    omuriceMenu.isPublished = true;
    state.lastUnlockedMenuId = omuriceMenu.id;
    state.lastUnlockedMenuDay = state.day;
  }

  const selectedMaidId = state.advChoiceHistory["main-chapter-2-delivery"];
  state.maids.forEach((maid) => {
    if (maid.task?.type === "researchRecipe") {
      maid.task = null;
      maid.bubbleText = TEXT.idle;
      moveMaid(maid, maid.homeX, maid.homeY);
    }
  });
  if (selectedMaidId && getMaidById(selectedMaidId)) {
    state.selectedResearchMaidId = selectedMaidId;
    changeMaidAffection(selectedMaidId, 5, "と一緒にオムライスを完成させました。");
  }

  state.selectedIngredients = [];
  state.recipeResult = "オムライスをメニューに追加しました。";
  state.recipeResultImage = omuriceMenu?.icon || "";
  pushLog("在庫管理とレシピ開発が使えるようになりました。", "機能解放");
  pushLog("オムライスを正式メニューに追加しました。", "メニュー");
  renderMaids();
  renderRecipeDevelopment();
  updateUI();
}

function finishChapterFourPromotionChoice() {
  if (state.systemUnlocks.promotion) {
    return;
  }
  state.systemUnlocks.promotion = true;
  queueSystemNotice(
    "unlock-promotion",
    "広報が解放されました！",
    "「広報」からは様々な広報活動が行えます。\nSNS投稿、チラシ配り、店頭ポスターなど、広報活動を行うことで来客数を増やせます。\n\n広報には費用・持続日数・リスクの違いがあります。\nSNSは無料ですが、評判が高くなると炎上する可能性があります。"
  );

  const selectedPlan = state.advChoiceHistory["main-first-turning-point"];
  if (selectedPlan === "poster") {
    changeMaidAffection("maid-1", 5, "の店頭ポスター案を採用しました。");
    applyPromotion("store-poster");
    pushLog("第4章をきっかけに、広報機能が解放されました。ことねの案で店頭ポスターを掲示します。", "機能解放");
  } else if (selectedPlan === "flyer") {
    changeMaidAffection("maid-2", 5, "のチラシ配り案を採用しました。");
    applyPromotion("flyer-handout");
    pushLog("第4章をきっかけに、広報機能が解放されました。すずの案でチラシ配りを始めます。", "機能解放");
  } else if (selectedPlan === "sns") {
    changeMaidAffection("maid-3", 5, "のSNS投稿案を採用しました。");
    applyPromotion("sns-post");
    pushLog("第4章をきっかけに、広報機能が解放されました。ひなの案でSNS投稿を始めます。", "機能解放");
  } else {
    pushLog("第4章をきっかけに、広報機能が解放されました。", "機能解放");
  }

  updateUI();
}

function finishChapterSevenSeatExpansion() {
  state.facilities.seating = Math.max(state.facilities.seating, 2);
  queueSystemNotice(
    "unlock-five-seats",
    "座席数を５席に拡張しました！",
    "店内の座席数が5席に増えました。\n一度に対応できるお客さんが増えるため、売上を伸ばしやすくなります。\n\nそのぶん注文・調理・配膳・片付けが重なりやすくなるので、メイドの手が足りなくならないよう注意してください。"
  );
  pushLog("第7章をきっかけに席配置を見直し、5席運用に拡張しました。", "席増設");
  updateUI();
}

function finishChapterEightSignatureChoice() {
  const selectedMaidId = state.advChoiceHistory["main-common-08-signature-menu"];
  if (!selectedMaidId || !getMaidById(selectedMaidId)) {
    return;
  }
  changeMaidAffection(selectedMaidId, 5, "を看板メニュー誕生の立役者として認めました。");
  updateUI();
}

function finishChapterNineNameSpreadChoice() {
  const selectedMaidId = state.advChoiceHistory["main-common-09-name-spreads"];
  if (!selectedMaidId || !getMaidById(selectedMaidId)) {
    return;
  }
  changeMaidAffection(selectedMaidId, 5, "と一緒に広がる評判への向き合い方を確かめました。");
  updateUI();
}

function finishChapterTenKitchenUpgrade() {
  if (state.chapterTenKitchenApplied) {
    return;
  }

  const selectedMaidId = state.advChoiceHistory["main-common-10-overconfidence"];
  if (selectedMaidId && getMaidById(selectedMaidId)) {
    changeMaidAffection(selectedMaidId, 5, "と一緒にキッチン強化の決断をしました。");
  }

  state.facilities.kitchen = Math.max(state.facilities.kitchen, 6);
  state.money = Math.max(0, state.money - 200000);
  state.facilityUpgradeCount += 1;
  state.chapterTenKitchenApplied = true;
  state.forceNextSnsFlame = true;
  queueSystemNotice(
    "unlock-kitchen-level-6",
    "キッチン設備を強化しました！",
    "キッチンの設備が強化され、調理時間が大幅に短縮されました。\n複数注文をまとめて調理する時も、以前より回しやすくなります。\n\nただし設備投資で資金が減っています。\n売上だけでなく、固定費と残り資金にも注意してください。"
  );
  pushLog("第10章をきっかけにキッチン設備をLv.6まで強化しました。資金 -200,000円", "設備投資");
  pushLog("第10章以降にSNS投稿を行うと、店の空気のズレが表面化しやすくなっています。", "不穏");
  updateUI();
}

function finishChapterElevenReputationPenalty() {
  if (state.chapterElevenReputationPenaltyApplied) {
    return;
  }

  state.chapterElevenReputationPenaltyApplied = true;
  state.hadSnsFlame = true;
  state.isFlamePenaltyActive = true;
  state.isChapterLockedFlamePenalty = true;
  state.flamePenaltyDaysRemaining = 0;
  state.postFlameProfitableDays = 0;
  state.activePromotions = state.activePromotions.filter((promotion) => promotion.id !== "sns-viral");
  changeReputation(-50, "第11章の炎上で、お店の評判が大きく落ち込みました。");
  pushLog("第11章を経て、炎上の影響が評判と客足に大きく反映されました。", "炎上");
  updateUI();
}

function nextAdvLine() {
  const scene = getAdvSceneById(state.activeAdvSceneId);
  const line = getCurrentAdvLine();
  if (!scene || !line || state.isAdvTransitioning || state.isAdvCgIntro || state.isAdvLineAutoWaiting || (Array.isArray(line.choices) && line.choices.length > 0)) {
    return;
  }

  if (isAdvInlineResponseActive()) {
    const nextInlineIndex = state.activeAdvInlineLineIndex + 1;
    if (nextInlineIndex < state.activeAdvInlineLines.length) {
    applyAfterAdvCgPrelude(scene, state.activeAdvInlineLines[nextInlineIndex], () => {
      state.activeAdvInlineLineIndex = nextInlineIndex;
      renderAdvScene();
    });
    return;
    }
    const returnIndex = typeof state.activeAdvInlineReturnIndex === "number"
      ? state.activeAdvInlineReturnIndex
      : state.activeAdvLineIndex + 1;
    clearAdvInlineResponse();
    if (returnIndex >= scene.lines.length) {
      completeAdvScene(scene);
      return;
    }
    applyAfterAdvCgPrelude(scene, scene.lines[returnIndex], () => {
      state.activeAdvLineIndex = returnIndex;
      renderAdvScene();
    });
    return;
  }

  if (state.activeAdvLineIndex >= scene.lines.length - 1) {
    completeAdvScene(scene);
    return;
  }

  const labeledNextIndex = getAdvLineIndexByLabel(scene, line.nextLabel);
  const directNextIndex = typeof line.nextIndex === "number" ? line.nextIndex : -1;
  const nextIndex = labeledNextIndex >= 0 ? labeledNextIndex : directNextIndex >= 0 ? directNextIndex : state.activeAdvLineIndex + 1;
  const nextLine = scene.lines[nextIndex] || null;
  const currentBackgroundId = getAdvLineBackgroundId(scene, line);
  const nextBackgroundId = getAdvLineBackgroundId(scene, nextLine);
  const applyLineChange = () => {
    state.activeAdvLineIndex = nextIndex;
    renderAdvScene();
  };

  if (currentBackgroundId !== nextBackgroundId) {
    prepareAdvCgPrelude(scene, nextLine);
    transitionAdvScene(applyLineChange, { fadeFrom: currentBackgroundId, fadeTo: nextBackgroundId });
    return;
  }

  applyAfterAdvCgPrelude(scene, nextLine, applyLineChange);
}

function chooseAdvOption(choiceIndex) {
  const scene = getAdvSceneById(state.activeAdvSceneId);
  const line = getCurrentAdvLine();
  const choice = line?.choices?.[choiceIndex];
  if (!scene || !choice || state.isAdvTransitioning || state.isAdvCgIntro) {
    return;
  }

  if (!state.isAdvDebugMode) {
    state.advChoiceHistory[scene.id] = choice.value || choice.label;
  }
  if (!state.isAdvDebugMode && choice.affectionDelta && choice.maidId) {
    changeMaidAffection(choice.maidId, choice.affectionDelta, choice.affectionReason || `${choice.label}を選びました。`);
  }
  const resolvedNextSceneId =
    scene.id?.match(/^night-talk-\d+$/) && choice.value && getMaidById(choice.value)
      ? getNightTalkChoiceNextSceneId(state.day, choice.value)
      : choice.nextSceneId;

  if (resolvedNextSceneId) {
    if (!state.isAdvDebugMode && !state.completedAdvSceneIds.includes(scene.id)) {
      state.completedAdvSceneIds.push(scene.id);
    }
    pushLog(`${scene.title} で「${choice.label}」を選びました。`, "ADV");
    if (!openAdvSceneById(resolvedNextSceneId, { advanceAfterClose: state.pendingAdvanceAfterAdv, debug: state.isAdvDebugMode })) {
      if (scene.id?.startsWith("night-talk-") && choice.value && getMaidById(choice.value)) {
        const fallbackSceneId = getNightTalkBranchSceneId(state.day, choice.value);
        pushLog(`${maidLabel(getMaidById(choice.value))}の個別イベントを開始できなかったため、通常の夜会話を表示します。`, "ADV");
        if (openAdvSceneById(fallbackSceneId, { advanceAfterClose: state.pendingAdvanceAfterAdv, debug: state.isAdvDebugMode })) {
          return;
        }
      }
      closeAdvModal();
    }
    return;
  }
  if (Array.isArray(choice.responseLines) && choice.responseLines.length > 0) {
    applyAfterAdvCgPrelude(scene, choice.responseLines[0], () => {
      state.activeAdvInlineLines = choice.responseLines.map((responseLine) => ({ ...responseLine }));
      state.activeAdvInlineLineIndex = 0;
      state.activeAdvInlineReturnIndex = state.activeAdvLineIndex + 1;
      renderAdvScene();
    });
    return;
  }
  const labeledNextIndex = getAdvLineIndexByLabel(scene, choice.nextLabel);
  const choiceNextIndex = labeledNextIndex >= 0 ? labeledNextIndex : choice.nextIndex;
  if (typeof choiceNextIndex !== "number" || choiceNextIndex >= scene.lines.length) {
    completeAdvScene(scene);
    return;
  }

  const nextLine = scene.lines[choiceNextIndex] || null;
  const currentBackgroundId = getAdvLineBackgroundId(scene, line);
  const nextBackgroundId = getAdvLineBackgroundId(scene, nextLine);
  const applyChoiceResult = () => {
    state.activeAdvLineIndex = choiceNextIndex;
    renderAdvScene();
  };

  if (currentBackgroundId !== nextBackgroundId) {
    prepareAdvCgPrelude(scene, nextLine);
    transitionAdvScene(applyChoiceResult, { fadeFrom: currentBackgroundId, fadeTo: nextBackgroundId });
    return;
  }

  applyAfterAdvCgPrelude(scene, nextLine, applyChoiceResult);
}

function renderFacilityList() {
  DOM.facilityList.innerHTML = "";
  FACILITY_DEFINITIONS.forEach((facility) => {
    const level = state.facilities[facility.id] || 0;
    const cost = getFacilityUpgradeCost(facility.id);
    const item = document.createElement("div");
    item.className = "facility-item";
    item.innerHTML = `
      <div class="facility-title-row">
        <div>
          <div class="facility-title">${facility.name}</div>
          <div class="facility-level">Lv.${level}</div>
        </div>
        <div class="facility-cost-chip">${yen(cost)}</div>
      </div>
      <div class="facility-meta">${facility.description}</div>
      <button class="facility-upgrade-btn" ${state.money < cost ? "disabled" : ""}>強化する</button>
    `;
    item.querySelector(".facility-upgrade-btn").addEventListener("click", () => upgradeFacility(facility.id));
    DOM.facilityList.appendChild(item);
  });
}

function upgradeFacility(facilityId) {
  const facility = getFacilityById(facilityId);
  const cost = getFacilityUpgradeCost(facilityId);
  if (!facility || state.money < cost) {
    return;
  }
  state.money -= cost;
  state.facilities[facilityId] += 1;
  state.facilityUpgradeCount += 1;
  pushLog(`${facility.name}をLv.${state.facilities[facilityId]}に強化しました。 資金 -${yen(cost)}`, "設備");
  renderFacilityList();
  updateUI();
}

function renderDailyReport() {
  const report = state.lastDailyReport;
  if (!report) {
    return;
  }

  DOM.dailyReportTitle.textContent = `${report.day}${TEXT.daySuffix} 日報`;
  DOM.dailyReportSales.textContent = yen(report.sales);
  DOM.dailyReportGuests.textContent = `${report.guests}人`;
  DOM.dailyReportReputation.textContent = `${report.reputationDelta > 0 ? "+" : ""}${report.reputationDelta}`;
  DOM.dailyReportMoney.textContent = yen(report.money);
  DOM.dailyReportFixedCosts.textContent = yen(report.fixedCosts);
  DOM.dailyReportProfit.textContent = `${report.profit >= 0 ? "+" : ""}${yen(report.profit)}`;
  DOM.dailyReportWalkouts.textContent = `${report.walkouts}人`;
  DOM.dailyReportSatisfaction.textContent = `${report.avgSatisfaction}`;
  DOM.dailyReportRanking.innerHTML = "";

  report.popularityRanking.forEach((menu, index) => {
    const item = document.createElement("div");
    item.className = "daily-report-rank-item";
    item.innerHTML = `
      <div class="daily-report-rank-main">
          <div class="daily-report-rank-icon"><img src="${menu.icon}" alt="${menu.name}"></div>
        <div>
          <div class="daily-report-rank-title">No.${index + 1} ${menu.name}</div>
          <div>${menu.dailyCount > 0 ? `本日 ${menu.dailyCount}品 / ` : ""}人気 ${menu.label}</div>
        </div>
      </div>
    `;
    DOM.dailyReportRanking.appendChild(item);
  });

  if (report.popularityRanking.length === 0) {
    DOM.dailyReportRanking.innerHTML = `<div class="daily-report-rank-item">まだ解放されているメニューがありません。</div>`;
  }

  DOM.dailyReportSummary.textContent = getDailySummary(report);
}

function renderDeliveryReport() {
  const report = state.lastDeliveryReport;
  if (!report || !DOM.deliveryReportItems || !DOM.deliveryReportSummary || !DOM.deliveryReportTitle) {
    return;
  }

  DOM.deliveryReportTitle.textContent = `${report.day}${TEXT.daySuffix} 朝の納品`;
  DOM.deliveryReportItems.innerHTML = "";

  report.items.forEach((item) => {
    const itemEl = document.createElement("div");
    itemEl.className = "delivery-report-item";
    itemEl.innerHTML = `
      <div class="delivery-report-item-icon"><img src="${item.icon}" alt="${item.name}"></div>
      <div>
        <div class="delivery-report-item-title">${item.name}</div>
        <div class="delivery-report-item-meta">${item.amount}個 納品済み</div>
      </div>
    `;
    DOM.deliveryReportItems.appendChild(itemEl);
  });

  const animatedItems = [DOM.deliveryBoxItemA, DOM.deliveryBoxItemB, DOM.deliveryBoxItemC];
  animatedItems.forEach((imageEl, index) => {
    if (!imageEl) {
      return;
    }
    const item = report.items[index];
    if (!item) {
      imageEl.removeAttribute("src");
      imageEl.alt = "";
      return;
    }
    imageEl.src = item.icon;
    imageEl.alt = item.name;
  });

  if (report.items.length === 0) {
    DOM.deliveryReportItems.innerHTML = `<div class="daily-report-summary">届いた食材はありません。</div>`;
  }

  DOM.deliveryReportSummary.textContent = report.moneyAfterPayment < 0
    ? `${report.orderedOnDay}${TEXT.daySuffix} に発注した食材が届き、支払い ${yen(report.totalCost)} が発生しました。資金が不足したため、この後ゲームオーバーになります。`
    : `${report.orderedOnDay}${TEXT.daySuffix} に発注した食材が届きました。合計支払額は ${yen(report.totalCost)} です。`;
}

function renderInventory() {
  DOM.inventoryList.innerHTML = "";
  renderInventoryDeliveryStatus();
  const isLockedForToday = state.hasOrderedToday;
  getUnlockedIngredients().forEach((ingredient) => {
    const marketPrice = getIngredientMarketPrice(ingredient.id);
    const priceDiff = marketPrice - ingredient.price;
    const orderLimit = Math.max(0, MAX_INVENTORY_AMOUNT - (state.inventory[ingredient.id] || 0));
    const pendingAmount = Math.min(state.pendingInventoryOrder[ingredient.id] || 0, orderLimit);
    const canAddInventory = !isLockedForToday && pendingAmount < orderLimit;
    const pendingCost = marketPrice * pendingAmount;
    const item = document.createElement("div");
    item.className = "inventory-item";
    item.innerHTML = `
      <div class="inventory-item-main">
        <img class="inventory-item-icon" src="${getIngredientIcon(ingredient.id)}" alt="${ingredient.name}">
        <div>
          <div>${ingredient.name}</div>
          <div class="inventory-meta">所持 ${state.inventory[ingredient.id] || 0} / 1個 ${yen(marketPrice)} (${priceDiff >= 0 ? "+" : ""}${yen(priceDiff)})</div>
        </div>
      </div>
      <div class="inventory-item-order">
        <div class="inventory-order-qty">予定 ${pendingAmount}個</div>
        <div class="inventory-meta">${yen(pendingCost)}</div>
      </div>
      <div class="inventory-stepper">
        <button class="inventory-step-btn" type="button" ${pendingAmount < 10 || isLockedForToday ? "disabled" : ""}>-10</button>
        <button class="inventory-step-btn" type="button" ${pendingAmount < 5 || isLockedForToday ? "disabled" : ""}>-5</button>
        <button class="inventory-step-btn" type="button" ${pendingAmount <= 0 || isLockedForToday ? "disabled" : ""}>-1</button>
        <button class="inventory-buy-btn" type="button" ${!canAddInventory ? "disabled" : ""}>+1</button>
        <button class="inventory-buy-btn" type="button" ${!canAddInventory ? "disabled" : ""}>+5</button>
        <button class="inventory-buy-btn" type="button" ${!canAddInventory ? "disabled" : ""}>+10</button>
      </div>
    `;
    const [minus10Btn, minus5Btn, minus1Btn] = item.querySelectorAll(".inventory-step-btn");
    const [plus1Btn, plus5Btn, plus10Btn] = item.querySelectorAll(".inventory-buy-btn");
    minus10Btn.addEventListener("click", () => {
      adjustInventoryOrder(ingredient.id, -10);
    });
    minus5Btn.addEventListener("click", () => {
      adjustInventoryOrder(ingredient.id, -5);
    });
    minus1Btn.addEventListener("click", () => {
      adjustInventoryOrder(ingredient.id, -1);
    });
    plus1Btn.addEventListener("click", () => {
      adjustInventoryOrder(ingredient.id, 1);
    });
    plus5Btn.addEventListener("click", () => {
      adjustInventoryOrder(ingredient.id, 5);
    });
    plus10Btn.addEventListener("click", () => {
      adjustInventoryOrder(ingredient.id, 10);
    });
    DOM.inventoryList.appendChild(item);
  });

  INGREDIENTS
    .filter((ingredient) => !state.unlockedIngredientIds.includes(ingredient.id))
    .sort((a, b) => a.unlockReputation - b.unlockReputation)
    .forEach((ingredient) => {
    const item = document.createElement("div");
    item.className = "inventory-item inventory-item-locked";
    item.innerHTML = `
      <div class="inventory-item-main">
        <img class="inventory-item-icon" src="${getIngredientIcon(ingredient.id)}" alt="${ingredient.name}">
        <div>
          <div>${ingredient.name}</div>
          <div class="inventory-meta">評判 ${ingredient.unlockReputation} で解放</div>
        </div>
      </div>
      <div class="inventory-meta">未解放</div>
      <div class="inventory-stepper">
        <button class="inventory-step-btn" type="button" disabled>-10</button>
        <button class="inventory-step-btn" type="button" disabled>-5</button>
        <button class="inventory-step-btn" type="button" disabled>-1</button>
        <button class="inventory-buy-btn" type="button" disabled>+1</button>
        <button class="inventory-buy-btn" type="button" disabled>+5</button>
        <button class="inventory-buy-btn" type="button" disabled>+10</button>
      </div>
    `;
    DOM.inventoryList.appendChild(item);
  });

  renderInventoryCart();
}

function adjustInventoryOrder(ingredientId, delta) {
  if (state.hasOrderedToday) {
    return;
  }
  const currentAmount = state.pendingInventoryOrder[ingredientId] || 0;
  const ownedAmount = state.inventory[ingredientId] || 0;
  const orderLimit = Math.max(0, MAX_INVENTORY_AMOUNT - ownedAmount);
  const nextAmount = Math.max(0, Math.min(orderLimit, currentAmount + delta));
  if (nextAmount === 0) {
    delete state.pendingInventoryOrder[ingredientId];
  } else {
    state.pendingInventoryOrder[ingredientId] = nextAmount;
  }
  renderInventory();
}

function getPendingInventoryOrderTotal() {
  return getCappedPendingInventoryOrderEntries().reduce((sum, [ingredientId, amount]) => {
    return sum + getIngredientMarketPrice(ingredientId) * amount;
  }, 0);
}

function getCappedPendingInventoryOrderEntries() {
  return Object.entries(state.pendingInventoryOrder)
    .map(([ingredientId, amount]) => {
      const ownedAmount = state.inventory[ingredientId] || 0;
      const orderLimit = Math.max(0, MAX_INVENTORY_AMOUNT - ownedAmount);
      return [ingredientId, Math.min(amount, orderLimit)];
    })
    .filter(([, amount]) => amount > 0);
}

function formatInventoryOrderSummary(orderEntries) {
  return orderEntries
    .map(([ingredientId, amount]) => `${getIngredientById(ingredientId)?.name || ingredientId} x${amount}`)
    .join(" / ");
}

function triggerBankruptcyBadEnd() {
  pushLog("資金が底をついたため、バッドエンドへ進みます。", "バッドエンド");
  if (!openAdvSceneById("ending-bad", { forceReplay: true, advanceAfterClose: false })) {
    showEndingThanksScreen("ending-bad");
  }
}

function triggerBankruptcyBadEndIfNeeded() {
  if (state.money >= 0) {
    return false;
  }
  triggerBankruptcyBadEnd();
  return true;
}

function renderInventoryDeliveryStatus() {
  if (!DOM.inventoryDeliveryStatus) {
    return;
  }

  const delivery = state.pendingInventoryDelivery;
  if (!delivery) {
    DOM.inventoryDeliveryStatus.classList.remove("is-ordered");
    DOM.inventoryDeliveryStatus.textContent = "本日の発注はまだ行っていません。発注した食材は翌朝に届きます。";
    return;
  }

  const summary = formatInventoryOrderSummary(Object.entries(delivery.items));
  DOM.inventoryDeliveryStatus.classList.add("is-ordered");
  DOM.inventoryDeliveryStatus.textContent = `本日は発注済みです。到着予定: ${delivery.arrivalDay}${TEXT.daySuffix}の朝 / 内容: ${summary}`;
}

function renderInventoryCart() {
  if (state.hasOrderedToday && state.pendingInventoryDelivery) {
    const orderedEntries = Object.entries(state.pendingInventoryDelivery.items).filter(([, amount]) => amount > 0);
    DOM.inventoryCartSummary.textContent = `${formatInventoryOrderSummary(orderedEntries)} / 支払い予定 ${yen(state.pendingInventoryDelivery.totalCost)} / 翌朝納品`;
    DOM.inventoryCartClearBtn.disabled = true;
    DOM.inventoryOrderBtn.disabled = true;
    return;
  }

  const orderEntries = getCappedPendingInventoryOrderEntries();
  if (orderEntries.length === 0) {
      DOM.inventoryCartSummary.textContent = "カートは空です";
      DOM.inventoryCartClearBtn.disabled = true;
      DOM.inventoryOrderBtn.disabled = true;
    return;
  }

  const summary = orderEntries
      .map(([ingredientId, amount]) => `${getIngredientById(ingredientId)?.name || ingredientId} x${amount}`)
      .join(" / ");
  const totalCost = getPendingInventoryOrderTotal();
  DOM.inventoryCartSummary.textContent = `${summary} / 支払い予定 ${yen(totalCost)} / 翌朝納品`;
  DOM.inventoryCartClearBtn.disabled = false;
  DOM.inventoryOrderBtn.disabled = state.money < totalCost;
}

function clearInventoryOrder() {
  if (state.hasOrderedToday) {
    return;
  }
  state.pendingInventoryOrder = {};
  renderInventory();
}

function scheduleScriptedInventoryOrder(items, logMessage = "") {
  if (state.hasOrderedToday || state.pendingInventoryDelivery) {
    return false;
  }
  const orderEntries = Object.entries(items)
    .map(([ingredientId, amount]) => {
      const ownedAmount = state.inventory[ingredientId] || 0;
      const orderLimit = Math.max(0, MAX_INVENTORY_AMOUNT - ownedAmount);
      return [ingredientId, Math.min(amount, orderLimit)];
    })
    .filter(([, amount]) => amount > 0);
  if (orderEntries.length === 0) {
    return false;
  }
  const totalCost = orderEntries.reduce((sum, [ingredientId, amount]) => {
    return sum + getIngredientMarketPrice(ingredientId) * amount;
  }, 0);
  if (state.money < totalCost) {
    return false;
  }
  state.pendingInventoryDelivery = {
    orderedOnDay: state.day,
    arrivalDay: state.day + 1,
    items: Object.fromEntries(orderEntries),
    totalCost,
  };
  state.hasOrderedToday = true;
  state.pendingInventoryOrder = Object.fromEntries(orderEntries);
  pushLog(
    logMessage || `${formatInventoryOrderSummary(orderEntries)} を発注しました。支払いと到着は翌朝です。`,
    "仕入れ"
  );
  renderInventory();
  renderRecipeDevelopment();
  updateUI();
  return true;
}

function applyPromotion(promotionId) {
  const promotion = getPromotionById(promotionId);
  if (!promotion || state.hasPromotedToday || state.money < promotion.cost) {
    return;
  }

  if (promotion.id === "sns-post" && getActivePromotion("sns-post")) {
    return;
  }

  const activePromotion = {
    id: promotion.id,
    name: promotion.name,
    daysRemaining: promotion.durationDays,
    startedOnDay: state.day,
    startsOnDay: promotion.id === "store-poster" ? state.day + 1 : state.day,
    recentRecipeBoost: promotion.id === "sns-post" && hasRecentMenuUnlock(),
    featuredMenuId: state.lastUnlockedMenuId || null,
  };

  state.money -= promotion.cost;
  state.hasPromotedToday = true;
  state.promotionUseCount += 1;
  if (promotion.id === "sns-post") {
    state.snsPostCount += 1;
  }
  state.activePromotions.push(activePromotion);
  pushLog(`${promotion.name}を実施しました。${promotion.cost > 0 ? `資金 -${yen(promotion.cost)}` : "費用はかかりません。"} `, "広報");
  if (promotion.id === "sns-post" && activePromotion.recentRecipeBoost) {
    pushLog("新メニュー直後のSNS投稿で、普段より注目を集めやすくなっています。", "SNS");
  }
  renderPromotionList();
  updateUI();
}

function resolveSnsOutcome() {
  const snsPromotion = getActivePromotion("sns-post");
  if (!snsPromotion) {
    return;
  }

  const avgSatisfaction = state.dailyFinishedGroups > 0 ? state.dailySatisfactionTotal / state.dailyFinishedGroups : 0;
  const poorReceptionScore = (avgSatisfaction < 0.8 ? 1 : 0) + (state.dailyWalkouts >= 2 ? 1 : 0) + (state.dailyGuests <= 2 ? 1 : 0);
  const goodReceptionScore = (avgSatisfaction >= 1.5 ? 1 : 0) + (state.dailyGuests >= 4 ? 1 : 0) + (snsPromotion.recentRecipeBoost ? 1 : 0);

  if (state.forceNextSnsFlame) {
    changeReputation(-12, "SNS投稿が炎上し、お店の評判が大きく下がりました。");
    state.lastSnsOutcome = { type: "flame", day: state.day, daysRemaining: 0 };
    state.snsFlameCount += 1;
    state.hadSnsFlame = true;
    state.isFlamePenaltyActive = true;
    state.isChapterLockedFlamePenalty = true;
    state.flamePenaltyDaysRemaining = 0;
    state.postFlameProfitableDays = 0;
    state.forceNextSnsFlame = false;
    pushLog("第10章後のSNS投稿が炎上し、店の空気のズレが一気に表面化しました。", "炎上");
  } else if (state.reputation >= 50 && poorReceptionScore >= 2 && Math.random() < 0.45) {
    changeReputation(-12, "SNS投稿が不評で炎上し、お店の評判が大きく下がりました。");
    state.lastSnsOutcome = { type: "flame", day: state.day, daysRemaining: 0 };
    state.snsFlameCount += 1;
    state.hadSnsFlame = true;
    state.isFlamePenaltyActive = true;
    state.isChapterLockedFlamePenalty = false;
    state.flamePenaltyDaysRemaining = 3;
    state.postFlameProfitableDays = 0;
    pushLog("SNS投稿が炎上してしまいました。しばらく慎重な運営が必要です。", "炎上");
  } else if (goodReceptionScore >= 2 && Math.random() < (snsPromotion.recentRecipeBoost ? 0.72 : 0.4)) {
    state.activePromotions.push({
      id: "sns-viral",
      name: "SNSバズ効果",
      daysRemaining: 3,
      startedOnDay: state.day,
      startsOnDay: state.day + 1,
      featuredMenuId: snsPromotion.featuredMenuId,
    });
    state.lastSnsOutcome = { type: "viral", day: state.day, daysRemaining: 3 };
    pushLog("SNS投稿がバズり、数日間は来客増加が見込めそうです。", "バズ");
  } else {
    state.lastSnsOutcome = { type: "normal", day: state.day, daysRemaining: 0 };
  }

  state.activePromotions = state.activePromotions.filter((promotion) => promotion.id !== "sns-post");
}

function finishChapterTwelveFlamePenalty() {
  if (!state.isFlamePenaltyActive || !state.isChapterLockedFlamePenalty) {
    return;
  }
  state.isFlamePenaltyActive = false;
  state.isChapterLockedFlamePenalty = false;
  state.flamePenaltyDaysRemaining = 0;
  pushLog("第12章を経て、炎上による評判の悪影響が少しずつ落ち着き始めました。", "立て直し");
  updateUI();
}

function finishChapterFourteenRecovery() {
  if (state.systemUnlocks.menuPublication) {
    return;
  }

  state.maids.forEach((maid) => {
    maid.stats.service = Math.min(MAX_MAID_STAT, (maid.stats.service || 0) + 1);
    maid.stats.cooking = Math.min(MAX_MAID_STAT, (maid.stats.cooking || 0) + 1);
  });
  state.systemUnlocks.menuPublication = true;
  queueSystemNotice(
    "unlock-menu-publication",
    "販売中・販売停止の切り替えが解放されました！",
    "レシピ一覧から、解放済みメニューの販売中・販売停止を切り替えられます。\n販売停止にしたメニューは注文されなくなります。\nただし、メニューが少ないとお客さんが帰ってしまうこともあります。\n\n材料不足の際に一時的に販売を中止したり、利益率が低いメニューを販売停止することで効率よく稼げるようになります。"
  );
  pushLog("第14章を経て、全メイドの接客と調理が1段階成長しました。", "成長");
  pushLog("第14章をきっかけに、レシピ一覧で販売中・販売停止を切り替えられるようになりました。", "機能解放");
  renderMaidList();
  renderRecipeBook();
  updateUI();
}

function getEndingSceneId() {
  const kotone = state.affection["maid-1"] || 0;
  const suzu = state.affection["maid-2"] || 0;
  const hina = state.affection["maid-3"] || 0;

  // ここで終盤のエンディング分岐条件を設定します。
  // 好感度の閾値はあとで見直しやすいよう、固定値をまとめて参照しています。
  const routeFourSeen = {
    "maid-1": state.completedAdvSceneIds.includes("kotone-route-04-ending"),
    "maid-2": state.completedAdvSceneIds.includes("suzu-route-04-ending"),
    "maid-3": state.completedAdvSceneIds.includes("hina-route-04-ending"),
  };

  if (routeFourSeen["maid-1"] && routeFourSeen["maid-2"] && routeFourSeen["maid-3"]) {
    return "ending-harem";
  }

  const eligible = [
    { id: "maid-1", value: kotone, ending: "ending-kotone" },
    { id: "maid-2", value: suzu, ending: "ending-suzu" },
    { id: "maid-3", value: hina, ending: "ending-hina" },
  ].filter((entry) => routeFourSeen[entry.id] && entry.value >= CHARACTER_ROUTE_AFFECTION_THRESHOLDS.ending);

  if (eligible.length > 0) {
    const maxAffection = Math.max(...eligible.map((entry) => entry.value));
    const topCandidates = eligible.filter((entry) => entry.value === maxAffection);
    const selected = topCandidates[Math.floor(Math.random() * topCandidates.length)];
    if (selected) {
      return selected.ending;
    }
  }

  return "ending-normal";
}

function getEndingThanksEventCgId(sceneId) {
  if (sceneId === "ending-bad") {
    return "";
  }
  return "";
}

function advancePromotionDurations() {
  state.activePromotions = state.activePromotions
    .map((promotion) => {
      const startsOnDay = promotion.startsOnDay || promotion.startedOnDay || 1;
      if (state.day <= startsOnDay) {
        return promotion;
      }
      return {
        ...promotion,
        daysRemaining: promotion.daysRemaining - 1,
      };
    })
    .filter((promotion) => promotion.daysRemaining > 0);
}

function submitInventoryOrder() {
  if (state.hasOrderedToday) {
    return;
  }
  const orderEntries = getCappedPendingInventoryOrderEntries();
  if (orderEntries.length === 0) {
    return;
  }

  const totalCost = orderEntries.reduce((sum, [ingredientId, amount]) => {
    return sum + getIngredientMarketPrice(ingredientId) * amount;
  }, 0);
  if (state.money < totalCost) {
    return;
  }

  scheduleScriptedInventoryOrder(
    Object.fromEntries(orderEntries),
    `${formatInventoryOrderSummary(orderEntries)} を発注しました。支払いと到着は翌朝です。`
  );
}

function openRecipeModal() {
  if (!state.systemUnlocks.recipeDevelopment) {
    return;
  }
  state.hasRecipeDevelopmentNotice = false;
  if (!getMaidById(state.selectedResearchMaidId) && state.maids[0]) {
    state.selectedResearchMaidId = state.maids[0].id;
  }
  state.selectedIngredients = [];
  state.isRecipeModalOpen = true;
  DOM.recipeModalBackdrop.classList.remove("hidden");
  updateActionButtonBadges();
  renderRecipeDevelopment();
  renderMaids();
}

function closeRecipeModal() {
  clearRecipeResultDisplay();
  state.isRecipeModalOpen = false;
  DOM.recipeModalBackdrop.classList.add("hidden");
  renderMaids();
}

function openRecipeBookModal() {
  state.isRecipeBookOpen = true;
  DOM.recipeBookModalBackdrop.classList.remove("hidden");
  renderRecipeBook();
}

function closeRecipeBookModal() {
  state.isRecipeBookOpen = false;
  DOM.recipeBookModalBackdrop.classList.add("hidden");
}

function openInventoryModal() {
  if (!state.systemUnlocks.inventoryManagement) {
    return;
  }
  state.hasInventoryNotice = false;
  state.isInventoryModalOpen = true;
  DOM.inventoryModalBackdrop.classList.remove("hidden");
  updateActionButtonBadges();
  renderInventory();
}

function closeInventoryModal() {
  state.isInventoryModalOpen = false;
  DOM.inventoryModalBackdrop.classList.add("hidden");
}

function renderPromotionList() {
  if (!DOM.promotionList) {
    return;
  }
  DOM.promotionStatus.textContent = getPromotionStatusText();
  DOM.promotionList.innerHTML = "";
  PROMOTION_DEFINITIONS.forEach((promotion) => {
    const activePromotion = getActivePromotion(promotion.id);
    const isDisabled =
      state.hasPromotedToday ||
      state.money < promotion.cost ||
      (promotion.id === "sns-post" && !!activePromotion);
    const item = document.createElement("div");
    item.className = "promotion-item";
    item.innerHTML = `
      <div class="promotion-item-title-row">
        <div class="promotion-item-title">${promotion.name}</div>
        <div class="promotion-item-cost">${yen(promotion.cost)}</div>
      </div>
      <div class="promotion-item-meta">${promotion.description}</div>
      <div class="promotion-item-effect">${promotion.effectSummary}</div>
      <div class="promotion-tags">${promotion.tags.map((tag) => `<span class="promotion-tag">${tag}</span>`).join("")}</div>
      <button class="promotion-apply-btn" type="button" ${isDisabled ? "disabled" : ""}>
        ${state.hasPromotedToday ? "本日は実施済み" : activePromotion ? (isPromotionEffectiveToday(activePromotion) ? `実施中 残り${activePromotion.daysRemaining}日` : "準備中 明日から開始") : "実施する"}
      </button>
    `;
    item.querySelector(".promotion-apply-btn")?.addEventListener("click", () => applyPromotion(promotion.id));
    DOM.promotionList.appendChild(item);
  });
}

function openPromotionModal() {
  if (!state.systemUnlocks.promotion) {
    return;
  }
  state.isPromotionModalOpen = true;
  DOM.promotionModalBackdrop?.classList.remove("hidden");
  renderPromotionList();
}

function closePromotionModal() {
  state.isPromotionModalOpen = false;
  DOM.promotionModalBackdrop?.classList.add("hidden");
}

function openMaidListModal() {
  state.isMaidListOpen = true;
  DOM.maidListModalBackdrop.classList.remove("hidden");
  renderMaidList();
}

function closeMaidListModal() {
  state.isMaidListOpen = false;
  DOM.maidListModalBackdrop.classList.add("hidden");
}

function openAdvModal() {
  const availableScenes = getAvailableAdvScenes();
  const unreadScene = availableScenes.find((scene) => !state.completedAdvSceneIds.includes(scene.id));
  openAdvSceneById(unreadScene?.id || availableScenes[0]?.id || null);
}

function openAdvDebugModal() {
  state.isAdvDebugMode = true;
  state.pendingAdvanceAfterAdv = false;
  state.isAdvModalOpen = true;
  state.activeAdvSceneId = null;
  state.activeAdvLineIndex = 0;
  clearAdvInlineResponse();
  DOM.advModal?.classList.add("is-debug");
  DOM.advModalBackdrop.classList.remove("hidden");
  DOM.advStage.classList.remove("is-faded");
  renderAdvSceneList();
  renderAdvScene();
  updateUI();
}

function closeAdvModal() {
  clearAdvTransitionTimer();
  clearAdvAutoTimer();
  clearAdvLineAutoTimer();
  clearAdvCgIntroTimer();
  state.isAdvModalOpen = false;
  state.isAdvTransitioning = false;
  state.isAdvDebugMode = false;
  state.isAdvAutoMode = false;
  state.activeAdvEventCgId = "";
  DOM.advModal?.classList.remove("is-debug");
  DOM.advModalBackdrop.classList.add("hidden");
  DOM.advStage.classList.remove("is-faded");
  DOM.advTextbox?.classList.remove("is-hidden-by-line");
  state.activeAdvSceneId = null;
  state.activeAdvLineIndex = 0;
  clearAdvInlineResponse();
  updateAdvAutoButton();
  updateUI();
  if (maybeOpenTrialEndDialog()) {
    return;
  }
  if (maybeOpenSystemNotice()) {
    return;
  }
  if (state.pendingAdvanceAfterAdv) {
    state.pendingAdvanceAfterAdv = false;
    if (state.daySettled && state.nextDayQueued) {
      advanceToNextDay();
    }
  }
}

function showEndingThanksScreen(endingSceneId = "ending-bad") {
  clearAdvTransitionTimer();
  clearAdvAutoTimer();
  clearAdvCgIntroTimer();
  state.isAdvModalOpen = false;
  state.isAdvTransitioning = false;
  state.isAdvDebugMode = false;
  state.isAdvAutoMode = false;
  state.pendingAdvanceAfterAdv = false;
  state.activeAdvEventCgId = "";
  state.activeAdvSceneId = null;
  state.activeAdvLineIndex = 0;
  clearAdvInlineResponse();
  DOM.advModal?.classList.remove("is-debug");
  DOM.advModalBackdrop?.classList.add("hidden");
  DOM.titleScreen?.classList.add("is-hidden");
  state.isTitleScreenOpen = false;
  state.isEndingThanksScreenOpen = true;
  const endingCgId = getEndingThanksEventCgId(endingSceneId);
  const endingCgAsset = getAdvEventCgAsset(endingCgId);
  if (DOM.endingThanksCg) {
    DOM.endingThanksCg.style.backgroundImage = endingCgAsset ? `url("${endingCgAsset}")` : "";
    DOM.endingThanksCg.style.animation = "none";
    DOM.endingThanksCg.offsetHeight;
    DOM.endingThanksCg.style.animation = "";
  }
  if (DOM.endingThanksLogo) {
    DOM.endingThanksLogo.src = ASSETS.ui.thankYouForPlaying;
    DOM.endingThanksLogo.classList.remove("is-missing");
  }
  DOM.endingThanksScreen?.classList.remove("hidden");
  updateAdvAutoButton();
  updateUI();
}

function returnToTitleAfterEnding() {
  if (!state.isEndingThanksScreenOpen) {
    return;
  }
  // エンディング後は現在の進行状態を破棄し、初期状態のタイトルへ戻します。
  window.location.reload();
}

function openFacilityModal() {
  state.isFacilityModalOpen = true;
  DOM.facilityModalBackdrop.classList.remove("hidden");
  renderFacilityList();
}

function closeFacilityModal() {
  state.isFacilityModalOpen = false;
  DOM.facilityModalBackdrop.classList.add("hidden");
}

function openDailyReportModal() {
  state.isDailyReportOpen = true;
  state.lastDailyReport = createDailyReport();
  DOM.dailyReportModalBackdrop.classList.remove("hidden");
  DOM.dailyReportSkipCheckbox.checked = state.skipDailyReport;
  renderDailyReport();
  if (state.dailyReportAutoCloseTimer) {
    clearTimeout(state.dailyReportAutoCloseTimer);
    state.dailyReportAutoCloseTimer = null;
  }
  if (state.skipDailyReport) {
    state.dailyReportAutoCloseTimer = setTimeout(() => {
      state.dailyReportAutoCloseTimer = null;
      if (state.isDailyReportOpen) {
        closeDailyReportModal();
      }
    }, 3000);
  }
}

function unlockAdvScene(sceneId) {
  if (!sceneId || state.unlockedAdvSceneIds.includes(sceneId)) {
    return;
  }
  state.unlockedAdvSceneIds.push(sceneId);
}

function getTriggeredMainSceneId(triggerType, day) {
  if (triggerType === "startup") {
    return "main-prologue-inheritance";
  }
  if (triggerType === "dailyReport") {
    if (day >= 1 && !state.completedAdvSceneIds.includes("main-first-arrival")) {
      return "main-first-arrival";
    }
    if (
      day >= 2 &&
      state.completedAdvSceneIds.includes("main-chapter-2-delivery") &&
      !state.completedAdvSceneIds.includes("main-grandfather-memory")
    ) {
      return "main-grandfather-memory";
    }
    if (
      day >= 3 &&
      state.completedAdvSceneIds.includes("main-first-turning-point") &&
      !hasCompletedAnyAdvScene(["main-promotion-results-poster", "main-promotion-results-flyer", "main-promotion-results-sns"])
    ) {
      const selectedPlan = state.advChoiceHistory["main-first-turning-point"];
      if (selectedPlan === "poster") {
        return "main-promotion-results-poster";
      }
      if (selectedPlan === "flyer") {
        return "main-promotion-results-flyer";
      }
      if (selectedPlan === "sns") {
        return "main-promotion-results-sns";
      }
    }
    if (day >= 4) {
      return getDynamicCommonMainSceneId("dailyReport");
    }
  }
  if (triggerType === "delivery") {
    if (
      day >= 2 &&
      state.completedAdvSceneIds.includes("main-first-arrival") &&
      !state.completedAdvSceneIds.includes("main-chapter-2-delivery")
    ) {
      return "main-chapter-2-delivery";
    }
    if (
      day >= 3 &&
      state.completedAdvSceneIds.includes("main-grandfather-memory") &&
      !state.completedAdvSceneIds.includes("main-first-turning-point")
    ) {
      return "main-first-turning-point";
    }
    if (day >= 4) {
      return getDynamicCommonMainSceneId("delivery");
    }
  }
  return null;
}

function getDynamicCommonMainSceneId(triggerType = "dailyReport") {
  const progression = [
  ];

  const nextScene = progression.find(
    (entry) =>
      (entry.triggerType || "dailyReport") === triggerType &&
      !state.completedAdvSceneIds.includes(entry.id) &&
      entry.condition()
  );
  return nextScene?.id || null;
}

function openAdvSceneById(sceneId, options = {}) {
  const scene = getAdvSceneById(sceneId);
  if (!scene) {
    return false;
  }
  if (!options.debug && !options.forceReplay && isMainAdvSceneId(sceneId) && state.completedAdvSceneIds.includes(sceneId)) {
    return false;
  }

  if (!options.debug) {
    unlockAdvScene(sceneId);
  }
  state.pendingAdvanceAfterAdv = Boolean(options.advanceAfterClose);
  state.isAdvModalOpen = true;
  state.isAdvDebugMode = Boolean(options.debug);
  DOM.advModal?.classList.toggle("is-debug", state.isAdvDebugMode);
  DOM.advModalBackdrop.classList.remove("hidden");
  DOM.advStage.classList.add("is-faded");
  if (DOM.advOpenBtn) {
    DOM.advOpenBtn.textContent = "ADVデバッグ";
  }
  state.activeAdvSceneId = sceneId;
  state.activeAdvLineIndex = 0;
  clearAdvLineAutoTimer();
  clearAdvCgIntroTimer();
  state.activeAdvEventCgId = "";
  clearAdvInlineResponse();
  renderAdvSceneList();
  renderAdvScene();
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      DOM.advStage.classList.remove("is-faded");
    });
  });
  return true;
}

function triggerMainStoryScene(triggerType, day, options = {}) {
  const sceneId = getTriggeredMainSceneId(triggerType, day);
  if (!sceneId || state.completedAdvSceneIds.includes(sceneId)) {
    return false;
  }
  return openAdvSceneById(sceneId, options);
}

function closeDailyReportModal() {
  if (state.dailyReportAutoCloseTimer) {
    clearTimeout(state.dailyReportAutoCloseTimer);
    state.dailyReportAutoCloseTimer = null;
  }
  state.isDailyReportOpen = false;
  DOM.dailyReportModalBackdrop.classList.add("hidden");
  if (triggerBankruptcyBadEndIfNeeded()) {
    return;
  }
  if (triggerMainStoryScene("dailyReport", state.day, { advanceAfterClose: true })) {
    return;
  }
  if (triggerNightTalkScene({ advanceAfterClose: true })) {
    return;
  }
  if (state.daySettled && state.nextDayQueued) {
    advanceToNextDay();
  }
  updateUI();
}

function openDeliveryReportModal() {
  if (!state.lastDeliveryReport || !DOM.deliveryReportModalBackdrop) {
    return;
  }

  state.isDeliveryReportOpen = true;
  DOM.deliveryReportModalBackdrop.classList.remove("hidden");
  DOM.deliveryReportModal?.classList.remove("is-opening");
  DOM.deliveryReportSkipCheckbox.checked = state.skipDeliveryReport;
  renderDeliveryReport();
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      DOM.deliveryReportModal?.classList.add("is-opening");
    });
  });

  if (state.deliveryReportAutoCloseTimer) {
    clearTimeout(state.deliveryReportAutoCloseTimer);
    state.deliveryReportAutoCloseTimer = null;
  }

  if (state.skipDeliveryReport) {
    state.deliveryReportAutoCloseTimer = setTimeout(() => {
      state.deliveryReportAutoCloseTimer = null;
      if (state.isDeliveryReportOpen) {
        closeDeliveryReportModal();
      }
    }, 3000);
  }
}

function closeDeliveryReportModal() {
  if (state.deliveryReportAutoCloseTimer) {
    clearTimeout(state.deliveryReportAutoCloseTimer);
    state.deliveryReportAutoCloseTimer = null;
  }
  state.isDeliveryReportOpen = false;
  DOM.deliveryReportModal?.classList.remove("is-opening");
  if (DOM.deliveryReportModalBackdrop) {
    DOM.deliveryReportModalBackdrop.classList.add("hidden");
  }
  if (state.pendingGameOverAfterDelivery) {
    state.pendingGameOverAfterDelivery = false;
    triggerBankruptcyBadEnd();
    return;
  }
  if (triggerBankruptcyBadEndIfNeeded()) {
    return;
  }
  if (triggerMainStoryScene("delivery", state.day)) {
    return;
  }
  updateUI();
}

function updateActionButtonBadges() {
  if (DOM.recipeOpenBtn) {
    DOM.recipeOpenBtn.textContent = `${state.hasRecipeDevelopmentNotice ? "✨ " : ""}レシピ開発`;
  }
  if (DOM.inventoryOpenBtn) {
    DOM.inventoryOpenBtn.textContent = `${state.hasInventoryNotice ? "✨ " : ""}在庫管理`;
  }
}

function updateDayOneLockedActions() {
  const hideRecipeFeature = !state.systemUnlocks.recipeDevelopment;
  const hideInventoryFeature = !state.systemUnlocks.inventoryManagement;
  const hidePromotionFeature = !state.systemUnlocks.promotion;
  updateActionButtonBadges();
  if (DOM.recipeOpenBtn) {
    DOM.recipeOpenBtn.hidden = hideRecipeFeature;
    DOM.recipeOpenBtn.disabled = hideRecipeFeature;
  }
  if (DOM.inventoryOpenBtn) {
    DOM.inventoryOpenBtn.hidden = hideInventoryFeature;
    DOM.inventoryOpenBtn.disabled = hideInventoryFeature;
  }
  if (DOM.promotionOpenBtn) {
    DOM.promotionOpenBtn.hidden = hidePromotionFeature;
    DOM.promotionOpenBtn.disabled = hidePromotionFeature;
  }
  if (DOM.maidListOpenBtn) {
    DOM.maidListOpenBtn.hidden = true;
    DOM.maidListOpenBtn.disabled = true;
  }
  if (DOM.facilityOpenBtn) {
    DOM.facilityOpenBtn.hidden = true;
    DOM.facilityOpenBtn.disabled = true;
  }
}

function setTimeScale(scale) {
  state.timeScale = scale;
  updateUI();
}

function hasSaveData() {
  return !!window.localStorage.getItem(SAVE_STORAGE_KEY);
}

function createMenuSaveSnapshot() {
  return MENUS.map((menu) => ({
    id: menu.id,
    unlocked: menu.unlocked,
    isPublished: menu.isPublished,
    popularity: menu.popularity,
  }));
}

function applyMenuSaveSnapshot(menuSnapshot = []) {
  const byId = Object.fromEntries(menuSnapshot.map((menu) => [menu.id, menu]));
  MENUS.forEach((menu) => {
    const saved = byId[menu.id];
    if (!saved) {
      return;
    }
    menu.unlocked = !!saved.unlocked;
    menu.isPublished = !!saved.isPublished;
    menu.popularity = typeof saved.popularity === "number" ? saved.popularity : menu.popularity;
  });
}

function createStateSaveSnapshot() {
  const snapshot = JSON.parse(JSON.stringify(state));
  snapshot.isTitleScreenOpen = false;
  snapshot.isRecipeModalOpen = false;
  snapshot.isRecipeBookOpen = false;
  snapshot.isAdvModalOpen = false;
  snapshot.isAdvTransitioning = false;
  snapshot.isAdvDebugMode = false;
  snapshot.isInventoryModalOpen = false;
  snapshot.isMaidListOpen = false;
  snapshot.isFacilityModalOpen = false;
  snapshot.isPromotionModalOpen = false;
  snapshot.isDailyReportOpen = false;
  snapshot.isDeliveryReportOpen = false;
  snapshot.isSystemNoticeOpen = false;
  snapshot.pendingAdvanceAfterAdv = false;
  snapshot.activeAdvSceneId = null;
  snapshot.activeAdvLineIndex = 0;
  snapshot.activeAdvInlineLines = [];
  snapshot.activeAdvInlineLineIndex = 0;
  snapshot.activeAdvInlineReturnIndex = null;
  snapshot.advTransitionTimer = null;
  snapshot.advAutoTimer = null;
  snapshot.advCgIntroTimer = null;
  snapshot.advLineAutoTimer = null;
  snapshot.activeAdvEventCgId = "";
  snapshot.isAdvCgIntro = false;
  snapshot.isAdvAutoMode = false;
  snapshot.isAdvLineAutoWaiting = false;
  snapshot.dailyReportAutoCloseTimer = null;
  snapshot.deliveryReportAutoCloseTimer = null;
  snapshot.lastDailyReport = null;
  snapshot.lastDeliveryReport = null;
  snapshot.activeSystemNotice = null;
  return snapshot;
}

function buildCheckpointPayload() {
  return {
    version: SAVE_DATA_VERSION,
    day: state.day,
    state: createStateSaveSnapshot(),
    menus: createMenuSaveSnapshot(),
  };
}

function captureDayStartCheckpoint() {
  dayStartCheckpoint = buildCheckpointPayload();
}

function hideAllModalBackdrops() {
  DOM.recipeModalBackdrop?.classList.add("hidden");
  DOM.recipeBookModalBackdrop?.classList.add("hidden");
  DOM.advModalBackdrop?.classList.add("hidden");
  DOM.inventoryModalBackdrop?.classList.add("hidden");
  DOM.maidListModalBackdrop?.classList.add("hidden");
  DOM.facilityModalBackdrop?.classList.add("hidden");
  DOM.promotionModalBackdrop?.classList.add("hidden");
  DOM.dailyReportModalBackdrop?.classList.add("hidden");
  DOM.deliveryReportModalBackdrop?.classList.add("hidden");
  DOM.systemNoticeModalBackdrop?.classList.add("hidden");
}

function clearTransientUiState() {
  clearAdvTransitionTimer();
  clearAdvAutoTimer();
  clearAdvLineAutoTimer();
  clearAdvCgIntroTimer();
  if (state.dailyReportAutoCloseTimer) {
    clearTimeout(state.dailyReportAutoCloseTimer);
    state.dailyReportAutoCloseTimer = null;
  }
  if (state.deliveryReportAutoCloseTimer) {
    clearTimeout(state.deliveryReportAutoCloseTimer);
    state.deliveryReportAutoCloseTimer = null;
  }
  state.isRecipeModalOpen = false;
  state.isTitleScreenOpen = false;
  state.isEndingThanksScreenOpen = false;
  state.isRecipeBookOpen = false;
  state.isAdvModalOpen = false;
  state.isAdvTransitioning = false;
  state.isAdvDebugMode = false;
  state.isInventoryModalOpen = false;
  state.isMaidListOpen = false;
  state.isFacilityModalOpen = false;
  state.isPromotionModalOpen = false;
  state.isDailyReportOpen = false;
  state.isDeliveryReportOpen = false;
  state.isSystemNoticeOpen = false;
  state.pendingAdvanceAfterAdv = false;
  state.activeAdvSceneId = null;
  state.activeAdvLineIndex = 0;
  state.activeAdvInlineLines = [];
  state.activeAdvInlineLineIndex = 0;
  state.activeAdvInlineReturnIndex = null;
  state.activeAdvEventCgId = "";
  state.isAdvAutoMode = false;
  state.isAdvLineAutoWaiting = false;
  state.lastDailyReport = null;
  state.lastDeliveryReport = null;
  state.activeSystemNotice = null;
  DOM.endingThanksScreen?.classList.add("hidden");
  if (DOM.endingThanksCg) {
    DOM.endingThanksCg.style.backgroundImage = "";
  }
  DOM.advTextbox?.classList.remove("is-hidden-by-line");
  hideAllModalBackdrops();
}

function processMorningStart() {
  const hadMorningDelivery = state.pendingInventoryDelivery && state.pendingInventoryDelivery.arrivalDay === state.day;
  if (hadMorningDelivery) {
    state.money -= state.pendingInventoryDelivery.totalCost;
    Object.entries(state.pendingInventoryDelivery.items).forEach(([ingredientId, amount]) => {
      state.inventory[ingredientId] = Math.min(MAX_INVENTORY_AMOUNT, (state.inventory[ingredientId] || 0) + amount);
    });
    state.lastDeliveryReport = createDeliveryReport(state.pendingInventoryDelivery);
    state.pendingGameOverAfterDelivery = state.money < 0;
    pushLog(`${formatInventoryOrderSummary(Object.entries(state.pendingInventoryDelivery.items))} が朝に届きました。支払い ${yen(state.lastDeliveryReport.totalCost)}`, "納品");
    state.pendingInventoryDelivery = null;
    openDeliveryReportModal();
    return;
  }

  state.lastDeliveryReport = null;
  triggerMainStoryScene("delivery", state.day);
}

function updateTitleContinueButton() {
  if (DOM.titleContinueBtn) {
    DOM.titleContinueBtn.disabled = !hasSaveData();
  }
}

function showTitleScreen() {
  state.isTitleScreenOpen = true;
  if (DOM.titleScreen) {
    DOM.titleScreen.classList.remove("is-hidden");
    requestAnimationFrame(() => {
      DOM.titleScreen?.classList.add("is-ready");
    });
  }
  updateTitleContinueButton();
}

function hideTitleScreen() {
  state.isTitleScreenOpen = false;
  DOM.titleScreen?.classList.add("is-hidden");
}

function startNewGameFromTitle() {
  hideTitleScreen();
  triggerMainStoryScene("startup", state.day);
  updateUI();
}

function continueGameFromTitle() {
  if (!hasSaveData()) {
    updateTitleContinueButton();
    return;
  }
  loadGameWithFade({ fromTitle: true });
}

function saveGame() {
  try {
    const payload = {
      ...(dayStartCheckpoint || buildCheckpointPayload()),
      savedAt: new Date().toISOString(),
    };
    window.localStorage.setItem(SAVE_STORAGE_KEY, JSON.stringify(payload));
    pushLog(`${payload.day}日目の朝データを保存しました。`, "システム");
    openSystemDialog("セーブしました", `${payload.day}日目の朝から再開できるように保存しました。`);
    updateUI();
  } catch (error) {
    console.error(error);
    pushLog("セーブに失敗しました。", "システム");
    openSystemDialog("セーブに失敗しました", "ブラウザの保存領域に書き込めませんでした。空き容量やブラウザ設定を確認してください。");
    updateUI();
  }
}

function deleteSaveData() {
  if (!hasSaveData()) {
    pushLog("削除できるセーブデータがありません。", "システム");
    updateUI();
    return;
  }

  const shouldDelete = window.confirm("セーブデータを削除しますか？");
  if (!shouldDelete) {
    return;
  }

  window.localStorage.removeItem(SAVE_STORAGE_KEY);
  pushLog("セーブデータを削除しました。", "システム");
  updateUI();
}

function wait(ms) {
  return new Promise((resolve) => window.setTimeout(resolve, ms));
}

function waitFrame() {
  return new Promise((resolve) => window.requestAnimationFrame(() => resolve()));
}

function setScreenFade(active) {
  DOM.screenFadeOverlay?.classList.toggle("is-active", active);
}

async function loadGameWithFade(options = {}) {
  if (!hasSaveData()) {
    pushLog("セーブデータがありません。", "システム");
    updateUI();
    return false;
  }

  setScreenFade(true);
  await wait(440);
  const loaded = loadGame({ ...options, useFade: true });
  await waitFrame();
  await wait(140);
  setScreenFade(false);
  return loaded;
}

function loadGame(options = {}) {
  const raw = window.localStorage.getItem(SAVE_STORAGE_KEY);
  if (!raw) {
    pushLog("セーブデータがありません。", "システム");
    updateUI();
    return false;
  }

  try {
    const payload = JSON.parse(raw);
    if (!payload?.state) {
      throw new Error("invalid save data");
    }

    clearTransientUiState();
    Object.keys(payload.state).forEach((key) => {
      state[key] = payload.state[key];
    });
    state.systemNoticeQueue = Array.isArray(state.systemNoticeQueue) ? state.systemNoticeQueue : [];
    state.shownSystemNoticeIds = Array.isArray(state.shownSystemNoticeIds) ? state.shownSystemNoticeIds : [];
    state.trialEndShown = Boolean(state.trialEndShown);
    state.activeSystemNotice = null;
    state.isSystemNoticeOpen = false;
    applyMenuSaveSnapshot(payload.menus);
    dayStartCheckpoint = {
      version: payload.version || SAVE_DATA_VERSION,
      day: payload.day || payload.state.day,
      state: JSON.parse(JSON.stringify(payload.state)),
      menus: JSON.parse(JSON.stringify(payload.menus || [])),
    };
    clearTransientUiState();
    if (options.fromTitle) {
      hideTitleScreen();
    }
    processMorningStart();
    pushLog(`${state.day}日目の朝から再開しました。`, "システム");
    updateUI();
    return true;
  } catch (error) {
    console.error(error);
    pushLog("ロードに失敗しました。", "システム");
    updateUI();
    return false;
  }
}

function updateUI() {
  if (DOM.advOpenBtn) {
    DOM.advOpenBtn.textContent = "ADVデバッグ";
    DOM.advOpenBtn.hidden = true;
    DOM.advOpenBtn.disabled = true;
  }
  updateDayOneLockedActions();
  // 上部UIは毎フレーム、現在時刻や営業状態に合わせて更新します。
  DOM.floor?.classList.toggle("is-night", isNightFloorTime());
  if (DOM.hallTitle) {
    const completedChapter = getCompletedMainChapterNumber();
    DOM.hallTitle.textContent = completedChapter > 0 ? `ホール（${completedChapter}章）` : "ホール";
  }
  DOM.clock.textContent = formatTime(state.currentMinute);
  DOM.money.textContent = yen(state.money);
  DOM.reputation.textContent = `${state.reputation}`;
  DOM.sales.textContent = yen(state.dailySales);
  DOM.speedPauseBtn.classList.toggle("active", state.timeScale === 0);
  DOM.speedNormalBtn.classList.toggle("active", state.timeScale === 1);
  DOM.speedFastBtn.classList.toggle("active", state.timeScale === 3);
  updateTitleContinueButton();
  const isTimeLocked =
    state.isTitleScreenOpen ||
    state.isEndingThanksScreenOpen ||
    state.isRecipeModalOpen ||
    state.isRecipeBookOpen ||
    state.isAdvModalOpen ||
    state.isInventoryModalOpen ||
    state.isMaidListOpen ||
    state.isFacilityModalOpen ||
    state.isPromotionModalOpen ||
    state.isDailyReportOpen ||
    state.isDeliveryReportOpen ||
    state.isSystemNoticeOpen;
  DOM.speedPauseBtn.disabled = isTimeLocked;
  DOM.speedNormalBtn.disabled = isTimeLocked;
  DOM.speedFastBtn.disabled = isTimeLocked;
  if (DOM.slgLoadBtn) {
    DOM.slgLoadBtn.disabled = !hasSaveData();
  }
  if (DOM.slgDeleteSaveBtn) {
    DOM.slgDeleteSaveBtn.disabled = !hasSaveData();
  }
  DOM.waitingLineIndicator.textContent = `行列 ${state.waitingCustomers.length}/${getWaitingCapacity()}`;
  const dayLabel = `${state.day}${TEXT.daySuffix}`;

  if (state.currentMinute < OPEN_MINUTE) {
    DOM.shopStatus.textContent = `${dayLabel} / ${TEXT.opening}`;
  } else if (isOpen()) {
    DOM.shopStatus.textContent = `${dayLabel} / ${TEXT.open}`;
  } else if (hasActiveCustomers()) {
    DOM.shopStatus.textContent = `${dayLabel} / ${TEXT.closing}`;
  } else {
    DOM.shopStatus.textContent = `${dayLabel} / ${TEXT.closed}`;
  }

  if (DOM.kitchenStatus) {
    DOM.kitchenStatus.textContent = state.activeKitchenOrder ? state.activeKitchenOrder.label : TEXT.idle;
  }
  if (DOM.dishPreview) {
    DOM.dishPreview.innerHTML = state.activeKitchenOrder
      ? `<img src="${state.activeKitchenOrder.icon}" alt="dish preview">`
      : "";
  }

  renderTables();
  renderMaids();
  renderLog();
  renderRecipeDevelopment();
  if (state.isInventoryModalOpen) {
    renderInventory();
  }
  if (state.isMaidListOpen) {
    renderMaidList();
  }
  if (state.isAdvModalOpen) {
    renderAdvScene();
  }
  if (state.isFacilityModalOpen) {
    renderFacilityList();
  }
  if (state.isPromotionModalOpen) {
    renderPromotionList();
  }
    if (state.isDailyReportOpen) {
      renderDailyReport();
    }
    if (state.isDeliveryReportOpen) {
      renderDeliveryReport();
    }
    updateCycleHighlights();
  }

function tick() {
  if (state.isTitleScreenOpen || state.isEndingThanksScreenOpen || state.isRecipeModalOpen || state.isRecipeBookOpen || state.isAdvModalOpen || state.isInventoryModalOpen || state.isMaidListOpen || state.isFacilityModalOpen || state.isPromotionModalOpen || state.isDailyReportOpen || state.isDeliveryReportOpen || state.isSystemNoticeOpen) {
      return;
    }

  // 倍速設定に応じて、1フレーム内でゲーム内時間を進めます。
  if (state.timeScale === 0) {
    updateUI();
    return;
  }

  if (state.currentMinute >= DAY_END_MINUTE && !hasActiveCustomers()) {
    if (state.daySettled && !state.nextDayQueued) {
      advanceToNextDay();
    }
    updateUI();
    return;
  }

  for (let step = 0; step < state.timeScale; step += 1) {
    if (state.currentMinute >= DAY_END_MINUTE && !hasActiveCustomers()) {
      break;
    }

    state.currentMinute += 1;

    if (state.currentMinute === OPEN_MINUTE) {
      pushLog("11:00 になりました。メイド喫茶の営業を開始します。", "開店");
    }

    if (state.currentMinute >= CLOSE_MINUTE && !state.closingLogged) {
      pushLog("20:00 になりました。新規来店を締め切ります。", "閉店");
      state.closingLogged = true;
    }

    updateWaitingCustomers();
    seatWaitingCustomerIfPossible();
    maybeSpawnCustomer();
    applyFinishedTasks();
    updateEatingCustomers();
    settleSales();
    startNextTasks();

    if (state.currentMinute >= CLOSE_MINUTE && !hasActiveCustomers() && !state.daySettled) {
      pauseResearchTasksForNextDay();
      resolveSnsOutcome();
      state.dailyFixedCosts = getFixedCostsForEndMinute(state.currentMinute);
      state.money += state.dailySales;
      pushLog(`本日の売上 ${yen(state.dailySales)} を資金に反映しました。`, "精算");
      state.money -= state.dailyFixedCosts;
      pushLog(`固定費 ${yen(state.dailyFixedCosts)} を支払いました。`, "経費");
      updateStoryProgressStatsForDay();
      state.daySettled = true;
      state.nextDayQueued = true;
      openDailyReportModal();
      break;
    }
  }

  updateUI();
}

function seedOpeningLog() {
  ensureInitialMenusAvailable();
  rollIngredientMarket();
  pushLog("今日の営業準備を始めます。", "準備");
  pushLog("11:00の開店に向けて、店内を整えています。", "準備");
  pushLog(`本日の固定費は${yen(state.dailyFixedCosts)}、仕入れ相場も更新されました。`, "経費");
}

window.addEventListener("resize", resizeGame);
DOM.developBtn.addEventListener("click", developRecipe);
DOM.recipeOpenBtn.addEventListener("click", openRecipeModal);
DOM.recipeCloseBtn.addEventListener("click", closeRecipeModal);
DOM.recipeModalBackdrop.addEventListener("click", (event) => {
  if (event.target === DOM.recipeModalBackdrop) {
    closeRecipeModal();
  }
});
DOM.recipeBookOpenBtn.addEventListener("click", openRecipeBookModal);
DOM.recipeBookCloseBtn.addEventListener("click", closeRecipeBookModal);
DOM.recipeBookModalBackdrop.addEventListener("click", (event) => {
  if (event.target === DOM.recipeBookModalBackdrop) {
    closeRecipeBookModal();
  }
});
DOM.advOpenBtn.addEventListener("click", openAdvDebugModal);
DOM.advCloseBtn.addEventListener("click", closeAdvModal);
DOM.advNextBtn.addEventListener("click", nextAdvLine);
DOM.advAutoBtn?.addEventListener("click", toggleAdvAutoMode);
DOM.advSkipBtn?.addEventListener("click", skipAdvScene);
DOM.advModal.addEventListener("click", (event) => {
  const currentLine = getCurrentAdvLine();
  if (Array.isArray(currentLine?.choices) && currentLine.choices.length > 0) {
    return;
  }
  if (
    event.target.closest(".adv-choice-btn") ||
    event.target.closest("#adv-close-btn") ||
    event.target.closest("#adv-skip-btn") ||
    event.target.closest("#adv-scene-list")
  ) {
    return;
  }
  nextAdvLine();
});
DOM.inventoryOpenBtn.addEventListener("click", openInventoryModal);
DOM.inventoryCloseBtn.addEventListener("click", closeInventoryModal);
DOM.inventoryCartClearBtn.addEventListener("click", clearInventoryOrder);
DOM.inventoryOrderBtn.addEventListener("click", submitInventoryOrder);
DOM.inventoryModalBackdrop.addEventListener("click", (event) => {
  if (event.target === DOM.inventoryModalBackdrop) {
    closeInventoryModal();
  }
});
DOM.maidListOpenBtn.addEventListener("click", openMaidListModal);
DOM.maidListCloseBtn.addEventListener("click", closeMaidListModal);
DOM.maidListModalBackdrop.addEventListener("click", (event) => {
  if (event.target === DOM.maidListModalBackdrop) {
    closeMaidListModal();
  }
});
DOM.facilityOpenBtn.addEventListener("click", openFacilityModal);
DOM.facilityCloseBtn.addEventListener("click", closeFacilityModal);
DOM.facilityModalBackdrop.addEventListener("click", (event) => {
  if (event.target === DOM.facilityModalBackdrop) {
    closeFacilityModal();
  }
});
DOM.promotionOpenBtn?.addEventListener("click", openPromotionModal);
DOM.promotionCloseBtn?.addEventListener("click", closePromotionModal);
DOM.promotionModalBackdrop?.addEventListener("click", (event) => {
  if (event.target === DOM.promotionModalBackdrop) {
    closePromotionModal();
  }
});
DOM.dailyReportCloseBtn.addEventListener("click", closeDailyReportModal);
DOM.dailyReportSkipCheckbox.addEventListener("change", (event) => {
  state.skipDailyReport = event.target.checked;
});
DOM.dailyReportModalBackdrop.addEventListener("click", (event) => {
  if (event.target === DOM.dailyReportModalBackdrop) {
    closeDailyReportModal();
  }
});
DOM.deliveryReportCloseBtn.addEventListener("click", closeDeliveryReportModal);
DOM.deliveryReportSkipCheckbox.addEventListener("change", (event) => {
  state.skipDeliveryReport = event.target.checked;
});
DOM.deliveryReportModalBackdrop.addEventListener("click", (event) => {
  if (event.target === DOM.deliveryReportModalBackdrop) {
    closeDeliveryReportModal();
  }
});
DOM.systemNoticeOkBtn?.addEventListener("click", closeSystemNotice);
DOM.speedPauseBtn.addEventListener("click", () => setTimeScale(0));
DOM.speedNormalBtn.addEventListener("click", () => setTimeScale(1));
DOM.speedFastBtn.addEventListener("click", () => setTimeScale(3));
DOM.titleNewGameBtn?.addEventListener("click", startNewGameFromTitle);
DOM.titleContinueBtn?.addEventListener("click", continueGameFromTitle);
DOM.endingThanksScreen?.addEventListener("click", returnToTitleAfterEnding);
DOM.titleLogo?.addEventListener("error", () => {
  DOM.titleLogo.classList.add("is-missing");
});
DOM.endingThanksLogo?.addEventListener("error", () => {
  DOM.endingThanksLogo.classList.add("is-missing");
});
DOM.slgSaveBtn?.addEventListener("click", saveGame);
DOM.slgLoadBtn?.addEventListener("click", () => loadGameWithFade());
DOM.slgDeleteSaveBtn?.addEventListener("click", deleteSaveData);

seedOpeningLog();
captureDayStartCheckpoint();
resizeGame();
updateUI();
showTitleScreen();
setInterval(tick, 1000 / MINUTES_PER_SECOND);
