// 市民オンライン手続きポータル — フロントエンド(バニラJS SPA)
// モバイルファースト。ハッシュルーティングで画面遷移する。

const TOKEN_KEY = "citizen-portal-token";
const DELIVERY_LABEL = { CONVENIENCE: "コンビニ交付", POSTAL: "郵送" };
const STATUS_LABEL = { RECEIVED: "受付", REVIEWING: "審査中", READY: "交付準備完了", DELIVERED: "交付済", CANCELLED: "取消", REJECTED: "却下" };

// ── 多言語対応(i18n): 日本語 / English / やさしい日本語 ──
const I18N = {
  ja: {
    "lang.label": "言語", "appbar.title": "市民オンライン手続き", "appbar.home": "ホームへ", "appbar.notif": "お知らせ", "appbar.logout": "ログアウト", "skip": "本文へスキップ", "menu": "メインメニュー",
    "nav.home": "ホーム", "nav.new": "申請する", "nav.estimate": "試算", "nav.list": "申請状況",
    "loading": "読み込み中…", "common.login": "ログイン", "common.loginProgress": "認証中…", "common.back": "← 戻る",
    "status.RECEIVED": "受付", "status.REVIEWING": "審査中", "status.READY": "交付準備完了", "status.DELIVERED": "交付済", "status.CANCELLED": "取消", "status.REJECTED": "却下",
    "delivery.CONVENIENCE": "コンビニ交付", "delivery.POSTAL": "郵送",
    "login.heroTitle": "来庁不要でできる、市民の手続き", "login.heroDesc": "マイナンバーカードで本人確認し、スマホやパソコンから証明書のオンライン請求などができます。",
    "login.jpki": "公的個人認証（JPKI）でログイン", "login.title": "マイナンバーカードでログイン", "login.hint": "本人確認は暗号化された電子証明書で行われます。",
    "login.cardNote": "ICカードリーダ／スマホのNFCでカードを読み取ります。本デモでは番号と暗証番号を入力してください。",
    "login.cardLabel": "マイナンバーカード番号（模擬）", "login.cardHint": "例: JPKI-0001 / JPKI-0002",
    "login.pinLabel": "利用者証明用パスワード（暗証番号4桁）", "login.pinHint": "3回続けて誤るとカードが一時ロックされます。",
    "login.demo": "デモ用: JPKI-0001 / 1234 ・ JPKI-0002 / 5678",
    "login.estTitle": "🧮 ログイン不要で試算", "login.estDesc": "保険料・手当・証明手数料の目安は、ログインせずに計算できます。入力内容は保存されません。", "login.estBtn": "保険料・手当を試算する",
    "toast.welcome": "ようこそ、{name}さん", "toast.loggedOut": "ログアウトしました。",
    "home.greeting": "こんにちは、{name}さん", "home.greetingDesc": "本人確認が完了しています。お手元のスマホ・パソコンから手続きできます。",
    "home.menuTitle": "手続きメニュー", "home.do": "手続きをする", "home.check": "申請状況を確認する",
    "home.menuDesc": "引越し・戸籍・子育て・健康・福祉・税など、本来は窓口での手続きが必要なものをまとめてオンライン化。来庁せずに行えます。",
    "home.estTitle": "🧮 保険料・手当の試算", "home.estDesc": "国保・後期・介護・年金の保険料や、児童手当・証明手数料の目安をその場で計算できます。", "home.estBtn": "試算してみる",
    "home.summaryTitle": "申請状況サマリー", "home.summaryDesc": "あなたの申請の現在の状況です。タップで一覧へ。",
    "home.freqTitle": "よく使う手続き", "home.freqDesc": "過去に申請した手続きにすぐアクセスできます。", "home.freqCount": "過去 {n} 回",
    "home.infoTitle": "登録されている本人情報", "home.infoDesc": "公的個人認証で確認した情報です。", "home.name": "氏名", "home.birth": "生年月日", "home.address": "住所",
    "list.title": "申請状況（{n}件）", "list.search": "手続き名・受付番号で検索", "list.all": "すべて", "list.empty": "まだ申請はありません。", "list.emptyBtn": "手続きをする", "list.noMatch": "該当する申請はありません。", "list.receipt": "受付番号",
    "notif.title": "お知らせ", "notif.desc": "申請の状況が変わると、ここでお知らせします。来庁の必要はありません。", "notif.empty": "お知らせはまだありません。", "notif.receipt": "受付番号",
    "notif.msg.RECEIVED": "申請を受け付けました。", "notif.msg.REVIEWING": "審査を開始しました。", "notif.msg.READY": "交付の準備が整いました。受け取り方法をご確認ください。", "notif.msg.DELIVERED": "交付が完了しました。", "notif.msg.REJECTED": "申請は受理されませんでした。詳細はお問い合わせください。",
    "est.hubTitle": "保険料・手当の試算", "est.hubDesc": "保険料・手当・手数料の目安をその場で計算できます。ログイン不要・入力内容は保存されません。窓口の算定と同じ計算式（令和6年度の標準的な数値）で試算します。", "est.loginNote": "手続きの申請にはログインが必要です。試算だけならこのままご利用いただけます。", "est.loginBtn": "ログインする",
    "form.hubTitle": "手続きを選ぶ",
    "form.backToList": "← 手続き一覧へ",
    "form.backShort": "← 一覧へ戻る",
    "form.applyNote": "この内容で担当課へオンライン申請します。受付後に処理状況を確認できます。来庁の必要はありません。",
    "form.applyBtn": "この内容で申請する",
    "detail.submittedAt": "申請日時",
    "detail.status": "処理状況",
    "detail.refresh": "状況を更新",
    "detail.reapply": "同じ内容で再申請",
    "detail.print": "🖨 受付票を印刷",
    "detail.cancel": "申請を取消す",
    "detail.copies": "部数",
    "detail.delivery": "受取方法",
    "detail.fee": "手数料",
    "detail.usage": "使いみち",
    "cert.title": "証明書のオンライン請求",
    "cert.step1": "① 証明書の種類",
    "cert.step3": "③ 受取方法",
    "cert.requestBtn": "この内容で請求する",
    "est.formBtn": "試算する",
    "est.backToMenu": "← 試算メニューへ",
  },
  en: {
    "lang.label": "Language", "appbar.title": "Online City Services", "appbar.home": "Home", "appbar.notif": "Notifications", "appbar.logout": "Log out", "skip": "Skip to content", "menu": "Main menu",
    "nav.home": "Home", "nav.new": "Apply", "nav.estimate": "Estimate", "nav.list": "Status",
    "loading": "Loading…", "common.login": "Log in", "common.loginProgress": "Authenticating…", "common.back": "← Back",
    "status.RECEIVED": "Received", "status.REVIEWING": "Under review", "status.READY": "Ready for pickup", "status.DELIVERED": "Issued", "status.CANCELLED": "Cancelled", "status.REJECTED": "Rejected",
    "delivery.CONVENIENCE": "Convenience-store pickup", "delivery.POSTAL": "By mail",
    "login.heroTitle": "City procedures without visiting the office", "login.heroDesc": "Verify your identity with the My Number Card and request certificates online from your phone or PC.",
    "login.jpki": "Sign in with Public Key Infrastructure (JPKI)", "login.title": "Sign in with the My Number Card", "login.hint": "Identity is verified with an encrypted electronic certificate.",
    "login.cardNote": "Read the card with an IC card reader or your phone's NFC. In this demo, enter the number and PIN.",
    "login.cardLabel": "My Number Card number (demo)", "login.cardHint": "e.g. JPKI-0001 / JPKI-0002",
    "login.pinLabel": "User authentication password (4-digit PIN)", "login.pinHint": "Three wrong attempts in a row will temporarily lock the card.",
    "login.demo": "Demo: JPKI-0001 / 1234 ・ JPKI-0002 / 5678",
    "login.estTitle": "🧮 Estimate without signing in", "login.estDesc": "You can estimate insurance premiums, allowances and certificate fees without signing in. Your input is not saved.", "login.estBtn": "Estimate premiums & allowances",
    "toast.welcome": "Welcome, {name}", "toast.loggedOut": "You have been logged out.",
    "home.greeting": "Hello, {name}", "home.greetingDesc": "Your identity is verified. You can complete procedures from your phone or PC.",
    "home.menuTitle": "Procedure menu", "home.do": "Start a procedure", "home.check": "Check application status",
    "home.menuDesc": "Moving, family register, childcare, health, welfare, tax and more — procedures that normally require an office visit, all online. No visit needed.",
    "home.estTitle": "🧮 Estimate premiums & allowances", "home.estDesc": "Estimate health/elderly/care/pension premiums, child allowance and certificate fees on the spot.", "home.estBtn": "Try the estimator",
    "home.summaryTitle": "Application status summary", "home.summaryDesc": "Current status of your applications. Tap to see the list.",
    "home.freqTitle": "Frequently used", "home.freqDesc": "Quick access to procedures you have applied for before.", "home.freqCount": "{n} time(s)",
    "home.infoTitle": "Your registered information", "home.infoDesc": "Information verified by public key authentication.", "home.name": "Name", "home.birth": "Date of birth", "home.address": "Address",
    "list.title": "Applications ({n})", "list.search": "Search by procedure name or receipt number", "list.all": "All", "list.empty": "No applications yet.", "list.emptyBtn": "Start a procedure", "list.noMatch": "No matching applications.", "list.receipt": "Receipt no.",
    "notif.title": "Notifications", "notif.desc": "When your application status changes, we will notify you here. No office visit needed.", "notif.empty": "No notifications yet.", "notif.receipt": "Receipt no.",
    "notif.msg.RECEIVED": "Your application has been received.", "notif.msg.REVIEWING": "Review has started.", "notif.msg.READY": "Ready for pickup. Please check how to receive it.", "notif.msg.DELIVERED": "Issuance is complete.", "notif.msg.REJECTED": "Your application was not accepted. Please contact us for details.",
    "est.hubTitle": "Estimate premiums & allowances", "est.hubDesc": "Estimate premiums, allowances and fees on the spot. No sign-in required and your input is not saved. The same formulas as the counter (standard FY2024 figures) are used.", "est.loginNote": "Signing in is required to apply. You can use the estimator without signing in.", "est.loginBtn": "Sign in",
    "form.hubTitle": "Choose a procedure",
    "form.backToList": "← Back to list",
    "form.backShort": "← Back to list",
    "form.applyNote": "This will be submitted online to the relevant department. You can check the status after submission. No office visit needed.",
    "form.applyBtn": "Submit this application",
    "detail.submittedAt": "Submitted",
    "detail.status": "Processing status",
    "detail.refresh": "Refresh status",
    "detail.reapply": "Re-apply with same details",
    "detail.print": "🖨 Print receipt",
    "detail.cancel": "Cancel application",
    "detail.copies": "Copies",
    "detail.delivery": "Pickup method",
    "detail.fee": "Fee",
    "detail.usage": "Purpose",
    "cert.title": "Request certificates online",
    "cert.step1": "① Type of certificate",
    "cert.step3": "③ Pickup method",
    "cert.requestBtn": "Request with these details",
    "est.formBtn": "Estimate",
    "est.backToMenu": "← Back to estimate menu",
  },
  easy: {
    "lang.label": "ことば", "appbar.title": "市役所(しやくしょ)の オンライン手(て)つづき", "appbar.home": "ホーム", "appbar.notif": "おしらせ", "appbar.logout": "ログアウト", "skip": "ほんぶんへ いく", "menu": "メニュー",
    "nav.home": "ホーム", "nav.new": "もうしこむ", "nav.estimate": "けいさん", "nav.list": "じょうきょう",
    "loading": "よみこみ中(ちゅう)…", "common.login": "ログイン", "common.loginProgress": "かくにん中(ちゅう)…", "common.back": "← もどる",
    "status.RECEIVED": "うけつけ", "status.REVIEWING": "しらべて います", "status.READY": "うけとり できます", "status.DELIVERED": "わたし おわり", "status.CANCELLED": "とりけし", "status.REJECTED": "できません でした",
    "delivery.CONVENIENCE": "コンビニで うけとり", "delivery.POSTAL": "ゆうびんで うけとり",
    "login.heroTitle": "市役所(しやくしょ)に いかなくても できる 手(て)つづき", "login.heroDesc": "マイナンバーカードで あなたを かくにんして、スマホや パソコンから しょるいを もうしこめます。",
    "login.jpki": "マイナンバーカードで ログイン", "login.title": "マイナンバーカードで ログイン", "login.hint": "あなたの かくにんは あんぜんな ほうほうで します。",
    "login.cardNote": "カードリーダーや スマホで カードを よみます。この デモでは ばんごうと あんしょうばんごうを いれて ください。",
    "login.cardLabel": "マイナンバーカードの ばんごう（れんしゅう）", "login.cardHint": "れい: JPKI-0001 / JPKI-0002",
    "login.pinLabel": "あんしょうばんごう（4けたの すうじ）", "login.pinHint": "3かい まちがえると カードが しばらく つかえなく なります。",
    "login.demo": "デモ: JPKI-0001 / 1234 ・ JPKI-0002 / 5678",
    "login.estTitle": "🧮 ログインしないで けいさん", "login.estDesc": "ほけんりょうや てあて、しょるいの おかねの めやすを、ログインしないで けいさんできます。いれた ことは ほぞんしません。", "login.estBtn": "ほけんりょう・てあてを けいさんする",
    "toast.welcome": "ようこそ、{name}さん", "toast.loggedOut": "ログアウト しました。",
    "home.greeting": "こんにちは、{name}さん", "home.greetingDesc": "あなたの かくにんが おわりました。スマホや パソコンで 手(て)つづき できます。",
    "home.menuTitle": "手(て)つづき メニュー", "home.do": "手(て)つづきを する", "home.check": "もうしこみの じょうきょうを みる",
    "home.menuDesc": "ひっこし・こせき・こそだて・けんこう・ふくし・ぜいきん など、ふつうは まどぐちで する 手(て)つづきを オンラインで できます。市役所(しやくしょ)に いかなくても いいです。",
    "home.estTitle": "🧮 ほけんりょう・てあての けいさん", "home.estDesc": "ほけんりょうや、こどもの てあて、しょるいの おかねの めやすを すぐ けいさん できます。", "home.estBtn": "けいさん して みる",
    "home.summaryTitle": "もうしこみの じょうきょう", "home.summaryDesc": "あなたの もうしこみの いまの じょうきょうです。タップで いちらんへ。",
    "home.freqTitle": "よく つかう 手(て)つづき", "home.freqDesc": "まえに もうしこんだ 手(て)つづきに すぐ いけます。", "home.freqCount": "まえに {n} かい",
    "home.infoTitle": "とうろくされている あなたの じょうほう", "home.infoDesc": "かくにんで わかった じょうほうです。", "home.name": "なまえ", "home.birth": "うまれた ひ", "home.address": "じゅうしょ",
    "list.title": "もうしこみ（{n}けん）", "list.search": "手(て)つづきの なまえ・うけつけ ばんごうで さがす", "list.all": "ぜんぶ", "list.empty": "まだ もうしこみは ありません。", "list.emptyBtn": "手(て)つづきを する", "list.noMatch": "みつかりません でした。", "list.receipt": "うけつけ ばんごう",
    "notif.title": "おしらせ", "notif.desc": "もうしこみの じょうきょうが かわると、ここで おしらせします。市役所(しやくしょ)に いかなくても いいです。", "notif.empty": "まだ おしらせは ありません。", "notif.receipt": "うけつけ ばんごう",
    "notif.msg.RECEIVED": "もうしこみを うけつけました。", "notif.msg.REVIEWING": "しらべ はじめました。", "notif.msg.READY": "うけとりの じゅんびが できました。うけとりかたを みて ください。", "notif.msg.DELIVERED": "わたし おわりました。", "notif.msg.REJECTED": "もうしこみは できませんでした。くわしくは きいて ください。",
    "est.hubTitle": "ほけんりょう・てあての けいさん", "est.hubDesc": "ほけんりょうや てあて、おかねの めやすを すぐ けいさん できます。ログインは いりません。いれた ことは ほぞんしません。", "est.loginNote": "もうしこむには ログインが いります。けいさんだけなら ログインしないで つかえます。", "est.loginBtn": "ログインする",
    "form.hubTitle": "手(て)つづきを えらぶ",
    "form.backToList": "← 手(て)つづき いちらんへ",
    "form.backShort": "← いちらんへ もどる",
    "form.applyNote": "この ないようで たんとうかへ オンラインで もうしこみます。あとで じょうきょうを みられます。市役所(しやくしょ)に いかなくても いいです。",
    "form.applyBtn": "この ないようで もうしこむ",
    "detail.submittedAt": "もうしこんだ ひにち",
    "detail.status": "しょりの じょうきょう",
    "detail.refresh": "じょうきょうを こうしん",
    "detail.reapply": "おなじ ないようで もういちど もうしこむ",
    "detail.print": "🖨 うけつけひょうを いんさつ",
    "detail.cancel": "もうしこみを とりけす",
    "detail.copies": "まいすう",
    "detail.delivery": "うけとり ほうほう",
    "detail.fee": "てすうりょう",
    "detail.usage": "つかいみち",
    "cert.title": "しょうめいしょの オンライン せいきゅう",
    "cert.step1": "① しょうめいしょの しゅるい",
    "cert.step3": "③ うけとり ほうほう",
    "cert.requestBtn": "この ないようで せいきゅうする",
    "est.formBtn": "けいさんする",
    "est.backToMenu": "← けいさん メニューへ",
  },
  zh: {
    "lang.label": "语言",
    "appbar.title": "市民在线办事",
    "appbar.home": "首页",
    "appbar.notif": "通知",
    "appbar.logout": "退出登录",
    "skip": "跳到正文",
    "menu": "主菜单",
    "nav.home": "首页",
    "nav.new": "申请",
    "nav.estimate": "试算",
    "nav.list": "申请状态",
    "loading": "加载中…",
    "common.login": "登录",
    "common.loginProgress": "认证中…",
    "common.back": "← 返回",
    "status.RECEIVED": "已受理",
    "status.REVIEWING": "审核中",
    "status.READY": "可领取",
    "status.DELIVERED": "已发放",
    "status.CANCELLED": "已取消",
    "status.REJECTED": "已驳回",
    "delivery.CONVENIENCE": "便利店领取",
    "delivery.POSTAL": "邮寄",
    "login.heroTitle": "无需到访政府的市民办事服务",
    "login.heroDesc": "使用My Number卡进行身份验证，可通过手机或电脑在线申请证明等。",
    "login.jpki": "使用公的个人认证（JPKI）登录",
    "login.title": "使用My Number卡登录",
    "login.hint": "通过加密的电子证书进行身份验证。",
    "login.cardNote": "用读卡器或手机NFC读取卡片。本演示请输入卡号和密码。",
    "login.cardLabel": "My Number卡号（模拟）",
    "login.cardHint": "例: JPKI-0001 / JPKI-0002",
    "login.pinLabel": "用户证明用密码（4位数字）",
    "login.pinHint": "连续输错3次卡将被临时锁定。",
    "login.demo": "演示用: JPKI-0001 / 1234 ・ JPKI-0002 / 5678",
    "login.estTitle": "🧮 无需登录即可试算",
    "login.estDesc": "保险费、补贴、证明手续费的预估金额无需登录即可计算。输入内容不会被保存。",
    "login.estBtn": "试算保险费・补贴",
    "toast.welcome": "欢迎，{name}",
    "toast.loggedOut": "已退出登录。",
    "home.greeting": "您好，{name}",
    "home.greetingDesc": "身份验证已完成。您可以通过手机或电脑办理手续。",
    "home.menuTitle": "办事菜单",
    "home.do": "办理手续",
    "home.check": "查看申请状态",
    "home.menuDesc": "搬迁・户籍・育儿・健康・福利・税务等原本需到窗口办理的手续，均可在线办理。无需到访。",
    "home.estTitle": "🧮 保险费・补贴试算",
    "home.estDesc": "可当场计算国保・后期・护理・养老金保险费、儿童补贴和证明手续费的预估。",
    "home.estBtn": "试算一下",
    "home.summaryTitle": "申请状态概览",
    "home.summaryDesc": "您的申请当前状态。点击查看列表。",
    "home.freqTitle": "常用手续",
    "home.freqDesc": "快速访问您以前申请过的手续。",
    "home.freqCount": "{n} 次",
    "home.infoTitle": "已登记的本人信息",
    "home.infoDesc": "通过公的个人认证确认的信息。",
    "home.name": "姓名",
    "home.birth": "出生日期",
    "home.address": "住址",
    "list.title": "申请状态（{n}件）",
    "list.search": "按手续名称・受理号搜索",
    "list.all": "全部",
    "list.empty": "尚无申请。",
    "list.emptyBtn": "办理手续",
    "list.noMatch": "没有符合的申请。",
    "list.receipt": "受理号",
    "notif.title": "通知",
    "notif.desc": "当申请状态变化时，将在此通知您。无需到访。",
    "notif.empty": "暂无通知。",
    "notif.receipt": "受理号",
    "notif.msg.RECEIVED": "您的申请已受理。",
    "notif.msg.REVIEWING": "已开始审核。",
    "notif.msg.READY": "已准备好领取。请确认领取方式。",
    "notif.msg.DELIVERED": "发放已完成。",
    "notif.msg.REJECTED": "申请未被受理。详情请咨询。",
    "est.hubTitle": "保险费・补贴试算",
    "est.hubDesc": "可当场计算保险费、补贴、手续费的预估。无需登录，输入内容不会被保存。采用与窗口相同的计算公式（令和6年度标准数值）。",
    "est.loginNote": "申请手续需要登录。仅试算可直接使用。",
    "est.loginBtn": "登录",
    "form.hubTitle": "选择手续",
    "form.backToList": "← 返回手续列表",
    "form.backShort": "← 返回列表",
    "form.applyNote": "将以此内容向主管科室在线申请。受理后可查看处理状态。无需到访。",
    "form.applyBtn": "以此内容申请",
    "detail.submittedAt": "申请时间",
    "detail.status": "处理状态",
    "detail.refresh": "刷新状态",
    "detail.reapply": "以相同内容再次申请",
    "detail.print": "🖨 打印受理单",
    "detail.cancel": "取消申请",
    "detail.copies": "份数",
    "detail.delivery": "领取方式",
    "detail.fee": "手续费",
    "detail.usage": "用途",
    "cert.title": "证明书在线申请",
    "cert.step1": "① 证明书种类",
    "cert.step3": "③ 领取方式",
    "cert.requestBtn": "以此内容申请",
    "est.formBtn": "试算",
    "est.backToMenu": "← 返回试算菜单",
  },
  ko: {
    "lang.label": "언어",
    "appbar.title": "시민 온라인 민원",
    "appbar.home": "홈",
    "appbar.notif": "알림",
    "appbar.logout": "로그아웃",
    "skip": "본문으로 건너뛰기",
    "menu": "메인 메뉴",
    "nav.home": "홈",
    "nav.new": "신청",
    "nav.estimate": "시산",
    "nav.list": "신청 현황",
    "loading": "불러오는 중…",
    "common.login": "로그인",
    "common.loginProgress": "인증 중…",
    "common.back": "← 뒤로",
    "status.RECEIVED": "접수",
    "status.REVIEWING": "심사 중",
    "status.READY": "수령 준비 완료",
    "status.DELIVERED": "발급 완료",
    "status.CANCELLED": "취소",
    "status.REJECTED": "반려",
    "delivery.CONVENIENCE": "편의점 수령",
    "delivery.POSTAL": "우편",
    "login.heroTitle": "방문 없이 가능한 시민 민원",
    "login.heroDesc": "마이넘버 카드로 본인 확인하고 스마트폰·PC에서 증명서 온라인 신청 등을 할 수 있습니다.",
    "login.jpki": "공적 개인인증(JPKI)으로 로그인",
    "login.title": "마이넘버 카드로 로그인",
    "login.hint": "본인 확인은 암호화된 전자증명서로 이루어집니다.",
    "login.cardNote": "IC 카드 리더기나 스마트폰 NFC로 카드를 읽습니다. 본 데모에서는 번호와 비밀번호를 입력하세요.",
    "login.cardLabel": "마이넘버 카드 번호(모의)",
    "login.cardHint": "예: JPKI-0001 / JPKI-0002",
    "login.pinLabel": "이용자 증명용 비밀번호(4자리 숫자)",
    "login.pinHint": "3회 연속 틀리면 카드가 일시적으로 잠깁니다.",
    "login.demo": "데모용: JPKI-0001 / 1234 ・ JPKI-0002 / 5678",
    "login.estTitle": "🧮 로그인 없이 시산",
    "login.estDesc": "보험료·수당·증명 수수료의 대략 금액을 로그인 없이 계산할 수 있습니다. 입력 내용은 저장되지 않습니다.",
    "login.estBtn": "보험료·수당 시산하기",
    "toast.welcome": "환영합니다, {name}님",
    "toast.loggedOut": "로그아웃되었습니다.",
    "home.greeting": "안녕하세요, {name}님",
    "home.greetingDesc": "본인 확인이 완료되었습니다. 스마트폰·PC에서 민원을 처리할 수 있습니다.",
    "home.menuTitle": "민원 메뉴",
    "home.do": "민원 신청하기",
    "home.check": "신청 현황 확인",
    "home.menuDesc": "이사·호적·육아·건강·복지·세금 등 원래 창구에서 해야 하는 민원을 온라인으로. 방문이 필요 없습니다.",
    "home.estTitle": "🧮 보험료·수당 시산",
    "home.estDesc": "국민건강보험·후기고령·요양·연금 보험료, 아동수당·증명 수수료의 대략을 즉시 계산할 수 있습니다.",
    "home.estBtn": "시산해 보기",
    "home.summaryTitle": "신청 현황 요약",
    "home.summaryDesc": "귀하의 신청 현재 상태입니다. 탭하면 목록으로.",
    "home.freqTitle": "자주 쓰는 민원",
    "home.freqDesc": "이전에 신청한 민원에 빠르게 접근합니다.",
    "home.freqCount": "{n}회",
    "home.infoTitle": "등록된 본인 정보",
    "home.infoDesc": "공적 개인인증으로 확인한 정보입니다.",
    "home.name": "이름",
    "home.birth": "생년월일",
    "home.address": "주소",
    "list.title": "신청 현황({n}건)",
    "list.search": "민원명·접수번호로 검색",
    "list.all": "전체",
    "list.empty": "아직 신청이 없습니다.",
    "list.emptyBtn": "민원 신청하기",
    "list.noMatch": "해당하는 신청이 없습니다.",
    "list.receipt": "접수번호",
    "notif.title": "알림",
    "notif.desc": "신청 상태가 바뀌면 여기서 알려드립니다. 방문이 필요 없습니다.",
    "notif.empty": "아직 알림이 없습니다.",
    "notif.receipt": "접수번호",
    "notif.msg.RECEIVED": "신청이 접수되었습니다.",
    "notif.msg.REVIEWING": "심사를 시작했습니다.",
    "notif.msg.READY": "수령 준비가 되었습니다. 수령 방법을 확인하세요.",
    "notif.msg.DELIVERED": "발급이 완료되었습니다.",
    "notif.msg.REJECTED": "신청이 수리되지 않았습니다. 자세한 내용은 문의하세요.",
    "est.hubTitle": "보험료·수당 시산",
    "est.hubDesc": "보험료·수당·수수료의 대략을 즉시 계산할 수 있습니다. 로그인 불필요, 입력 내용은 저장되지 않습니다. 창구와 같은 계산식(레이와 6년도 표준 수치)을 사용합니다.",
    "est.loginNote": "민원 신청에는 로그인이 필요합니다. 시산만이라면 그대로 이용할 수 있습니다.",
    "est.loginBtn": "로그인",
    "form.hubTitle": "민원 선택",
    "form.backToList": "← 민원 목록으로",
    "form.backShort": "← 목록으로",
    "form.applyNote": "이 내용으로 담당과에 온라인 신청합니다. 접수 후 처리 상황을 확인할 수 있습니다. 방문이 필요 없습니다.",
    "form.applyBtn": "이 내용으로 신청",
    "detail.submittedAt": "신청 일시",
    "detail.status": "처리 상황",
    "detail.refresh": "상황 새로고침",
    "detail.reapply": "같은 내용으로 재신청",
    "detail.print": "🖨 접수증 인쇄",
    "detail.cancel": "신청 취소",
    "detail.copies": "부수",
    "detail.delivery": "수령 방법",
    "detail.fee": "수수료",
    "detail.usage": "용도",
    "cert.title": "증명서 온라인 청구",
    "cert.step1": "① 증명서 종류",
    "cert.step3": "③ 수령 방법",
    "cert.requestBtn": "이 내용으로 청구",
    "est.formBtn": "시산",
    "est.backToMenu": "← 시산 메뉴로",
  },
  vi: {
    "lang.label": "Ngôn ngữ",
    "appbar.title": "Dịch vụ công dân trực tuyến",
    "appbar.home": "Trang chủ",
    "appbar.notif": "Thông báo",
    "appbar.logout": "Đăng xuất",
    "skip": "Bỏ qua đến nội dung",
    "menu": "Menu chính",
    "nav.home": "Trang chủ",
    "nav.new": "Nộp đơn",
    "nav.estimate": "Ước tính",
    "nav.list": "Trạng thái",
    "loading": "Đang tải…",
    "common.login": "Đăng nhập",
    "common.loginProgress": "Đang xác thực…",
    "common.back": "← Quay lại",
    "status.RECEIVED": "Đã tiếp nhận",
    "status.REVIEWING": "Đang xét duyệt",
    "status.READY": "Sẵn sàng nhận",
    "status.DELIVERED": "Đã cấp",
    "status.CANCELLED": "Đã hủy",
    "status.REJECTED": "Bị từ chối",
    "delivery.CONVENIENCE": "Nhận tại cửa hàng tiện lợi",
    "delivery.POSTAL": "Qua bưu điện",
    "login.heroTitle": "Thủ tục công dân không cần đến quầy",
    "login.heroDesc": "Xác minh danh tính bằng thẻ My Number và yêu cầu giấy tờ trực tuyến từ điện thoại hoặc máy tính.",
    "login.jpki": "Đăng nhập bằng xác thực cá nhân công (JPKI)",
    "login.title": "Đăng nhập bằng thẻ My Number",
    "login.hint": "Danh tính được xác minh bằng chứng thư điện tử được mã hóa.",
    "login.cardNote": "Đọc thẻ bằng đầu đọc IC hoặc NFC của điện thoại. Trong bản demo này, hãy nhập số và mã PIN.",
    "login.cardLabel": "Số thẻ My Number (demo)",
    "login.cardHint": "VD: JPKI-0001 / JPKI-0002",
    "login.pinLabel": "Mật khẩu xác thực người dùng (PIN 4 chữ số)",
    "login.pinHint": "Nhập sai 3 lần liên tiếp thẻ sẽ tạm khóa.",
    "login.demo": "Demo: JPKI-0001 / 1234 ・ JPKI-0002 / 5678",
    "login.estTitle": "🧮 Ước tính không cần đăng nhập",
    "login.estDesc": "Bạn có thể ước tính phí bảo hiểm, trợ cấp và lệ phí giấy tờ mà không cần đăng nhập. Dữ liệu nhập không được lưu.",
    "login.estBtn": "Ước tính phí bảo hiểm & trợ cấp",
    "toast.welcome": "Chào mừng, {name}",
    "toast.loggedOut": "Bạn đã đăng xuất.",
    "home.greeting": "Xin chào, {name}",
    "home.greetingDesc": "Danh tính của bạn đã được xác minh. Bạn có thể làm thủ tục từ điện thoại hoặc máy tính.",
    "home.menuTitle": "Menu thủ tục",
    "home.do": "Bắt đầu thủ tục",
    "home.check": "Kiểm tra trạng thái đơn",
    "home.menuDesc": "Chuyển nhà, hộ tịch, nuôi con, sức khỏe, phúc lợi, thuế… các thủ tục vốn cần đến quầy, nay đều trực tuyến. Không cần đến nơi.",
    "home.estTitle": "🧮 Ước tính phí bảo hiểm & trợ cấp",
    "home.estDesc": "Ước tính ngay phí bảo hiểm y tế/người cao tuổi/chăm sóc/hưu trí, trợ cấp trẻ em và lệ phí giấy tờ.",
    "home.estBtn": "Thử ước tính",
    "home.summaryTitle": "Tóm tắt trạng thái đơn",
    "home.summaryDesc": "Trạng thái hiện tại của đơn của bạn. Chạm để xem danh sách.",
    "home.freqTitle": "Thường dùng",
    "home.freqDesc": "Truy cập nhanh các thủ tục bạn đã nộp trước đây.",
    "home.freqCount": "{n} lần",
    "home.infoTitle": "Thông tin đã đăng ký của bạn",
    "home.infoDesc": "Thông tin được xác minh bằng xác thực cá nhân công.",
    "home.name": "Họ tên",
    "home.birth": "Ngày sinh",
    "home.address": "Địa chỉ",
    "list.title": "Đơn ({n})",
    "list.search": "Tìm theo tên thủ tục hoặc số tiếp nhận",
    "list.all": "Tất cả",
    "list.empty": "Chưa có đơn nào.",
    "list.emptyBtn": "Bắt đầu thủ tục",
    "list.noMatch": "Không có đơn phù hợp.",
    "list.receipt": "Số tiếp nhận",
    "notif.title": "Thông báo",
    "notif.desc": "Khi trạng thái đơn thay đổi, chúng tôi sẽ thông báo tại đây. Không cần đến quầy.",
    "notif.empty": "Chưa có thông báo.",
    "notif.receipt": "Số tiếp nhận",
    "notif.msg.RECEIVED": "Đơn của bạn đã được tiếp nhận.",
    "notif.msg.REVIEWING": "Đã bắt đầu xét duyệt.",
    "notif.msg.READY": "Đã sẵn sàng để nhận. Vui lòng kiểm tra cách nhận.",
    "notif.msg.DELIVERED": "Đã cấp xong.",
    "notif.msg.REJECTED": "Đơn không được chấp nhận. Vui lòng liên hệ để biết chi tiết.",
    "est.hubTitle": "Ước tính phí bảo hiểm & trợ cấp",
    "est.hubDesc": "Ước tính ngay phí bảo hiểm, trợ cấp và lệ phí. Không cần đăng nhập và dữ liệu nhập không được lưu. Dùng cùng công thức như tại quầy (số liệu chuẩn năm Reiwa 6).",
    "est.loginNote": "Cần đăng nhập để nộp đơn. Bạn có thể dùng công cụ ước tính mà không cần đăng nhập.",
    "est.loginBtn": "Đăng nhập",
    "form.hubTitle": "Chọn thủ tục",
    "form.backToList": "← Về danh sách",
    "form.backShort": "← Về danh sách",
    "form.applyNote": "Đơn sẽ được nộp trực tuyến đến bộ phận phụ trách. Bạn có thể kiểm tra trạng thái sau khi nộp. Không cần đến quầy.",
    "form.applyBtn": "Nộp đơn này",
    "detail.submittedAt": "Thời gian nộp",
    "detail.status": "Trạng thái xử lý",
    "detail.refresh": "Cập nhật trạng thái",
    "detail.reapply": "Nộp lại cùng nội dung",
    "detail.print": "🖨 In biên nhận",
    "detail.cancel": "Hủy đơn",
    "detail.copies": "Số bản",
    "detail.delivery": "Cách nhận",
    "detail.fee": "Lệ phí",
    "detail.usage": "Mục đích",
    "cert.title": "Yêu cầu giấy chứng nhận trực tuyến",
    "cert.step1": "① Loại giấy",
    "cert.step3": "③ Cách nhận",
    "cert.requestBtn": "Yêu cầu với nội dung này",
    "est.formBtn": "Ước tính",
    "est.backToMenu": "← Về menu ước tính",
  },
};
let lang = localStorage.getItem("portal-lang") || "ja";
function t(key, vars) {
  let s = (I18N[lang] && I18N[lang][key]) || I18N.ja[key] || key;
  if (vars) for (const [k, v] of Object.entries(vars)) s = s.split(`{${k}}`).join(String(v));
  return s;
}
const tStatus = (s) => t("status." + s) || s;
const tDelivery = (d) => t("delivery." + d) || d;
function applyStaticI18n() {
  document.querySelectorAll("[data-i18n]").forEach((el) => { el.textContent = t(el.dataset.i18n); });
  document.querySelectorAll("[data-i18n-aria]").forEach((el) => { el.setAttribute("aria-label", t(el.dataset.i18nAria)); });
  const sel = document.getElementById("lang-select"); if (sel) sel.value = lang;
  document.documentElement.lang = lang === "en" ? "en" : "ja";
}
function setLang(l) {
  lang = l; localStorage.setItem("portal-lang", l);
  applyStaticI18n(); render();
}

const state = { token: localStorage.getItem(TOKEN_KEY) || null, me: null, types: [], prefill: null };

const $ = (sel, root = document) => root.querySelector(sel);
const view = $("#view");

function token() { return state.token; }
function setToken(t) {
  state.token = t;
  if (t) localStorage.setItem(TOKEN_KEY, t); else localStorage.removeItem(TOKEN_KEY);
}

async function api(path, { method = "GET", body } = {}) {
  const headers = { "content-type": "application/json" };
  if (token()) headers.authorization = `Bearer ${token()}`;
  const res = await fetch(`/api${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw Object.assign(new Error(data.error || `エラー(${res.status})`), { status: res.status });
  return data;
}

function toast(msg, isError = false) {
  const el = $("#toast");
  el.textContent = msg;
  el.classList.toggle("toast--err", isError);
  el.hidden = false;
  clearTimeout(toast._t);
  toast._t = setTimeout(() => { el.hidden = true; }, 3200);
}

function esc(s) { return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c])); }
function yen(n) { return `${Number(n).toLocaleString("ja-JP")}円`; }

// 申請の受付票（控え）を印刷する。証明書請求・届出の両方に対応。
function openReceipt(a) {
  const m = state.me || {};
  const lines = [
    ["受付番号", a.receiptNumber],
    ["手続き", a.typeName + (a.copies ? `（${a.copies}部）` : "")],
    ["申請者", `${m.familyNameKanji || ""} ${m.givenNameKanji || ""}`],
    ["申請日時", new Date(a.submittedAt).toLocaleString("ja-JP")],
    ["状態", tStatus(a.status)],
  ];
  if (a.deliveryMethod) lines.push(["受取方法", tDelivery(a.deliveryMethod)]);
  if (typeof a.fee === "number") lines.push(["手数料", yen(a.fee)]);
  if (a.payload) for (const [k, v] of Object.entries(a.payload)) { if (v) lines.push([k, v]); }
  const rows = lines.map(([k, v]) => `<tr><th>${esc(k)}</th><td>${esc(v)}</td></tr>`).join("");
  document.getElementById("receipt").innerHTML = `
    <div class="receipt-doc">
      <p class="receipt-eyebrow">市民オンライン手続きポータル</p>
      <h1>受付票（控え）</h1>
      <table class="receipt-table">${rows}</table>
      <p class="receipt-note">この受付票は申請の控えです。処理状況はポータルでご確認いただけます。来庁の必要はありません。</p>
      <p class="receipt-note small">発行日時: ${new Date().toLocaleString("ja-JP")}</p>
    </div>`;
  window.print();
}

// ── ルーティング ──
function go(hash) { if (location.hash === `#${hash}`) render(); else location.hash = hash; }
window.addEventListener("hashchange", render);

function setNav(active) {
  document.querySelectorAll("[data-nav]").forEach((b) => {
    b.setAttribute("aria-current", b.dataset.nav === active ? "page" : "false");
  });
}

function syncChrome() {
  const userArea = $("#user-area");
  const tabbar = $("#tabbar");
  if (state.me) {
    userArea.hidden = false;
    tabbar.hidden = false;
    $("#user-name").textContent = `${state.me.familyNameKanji} ${state.me.givenNameKanji}${lang==="en"?"":" さん"}`;
    updateNotifBadge();
  } else {
    userArea.hidden = true;
    tabbar.hidden = true;
  }
}

function loading() { view.innerHTML = `<div class="spinner">${t("loading")}</div>`; }

async function render() {
  const route = (location.hash || "#home").slice(1);
  // 認証必須ルート
  if (!state.me && route !== "login") {
    if (token()) {
      try { state.me = await api("/me"); } catch { setToken(null); }
    }
  }
  syncChrome();

  // 制度の試算は計算のみ（個人情報を保存しない）ため、ログイン前でも利用できる
  if (route === "estimate") return renderEstimateHub();
  if (route.startsWith("estimate/")) return renderEstimateForm(route.slice(9));
  if (!state.me) return renderLogin();
  if (route.startsWith("detail/")) return renderDetail(route.slice(7));
  if (route.startsWith("pdetail/")) return renderProcDetail(route.slice(8));
  if (route.startsWith("proc/")) return renderProcForm(route.slice(5));
  if (route === "cert") return renderNew();
  if (route === "new") return renderHub();
  if (route === "list") return renderList();
  if (route === "notifications") return renderNotifications();
  return renderHome();
}

// ── ログイン（マイナンバーカード認証）──
function renderLogin() {
  setNav("");
  view.innerHTML = `
    <section class="card hero">
      <h1>${t("login.heroTitle")}</h1>
      <p>${t("login.heroDesc")}</p>
    </section>
    <section class="card">
      <div class="badge-secure">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 2l7 3v6c0 5-3.5 8.5-7 9-3.5-.5-7-4-7-9V5l7-3z" fill="currentColor"/><path d="M9 12l2 2 4-4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
        ${t("login.jpki")}
      </div>
      <h2 class="section-title">${t("login.title")}</h2>
      <p class="hint small">${t("login.hint")}</p>
      <div class="mynbr">
        <svg width="62" height="40" viewBox="0 0 62 40" role="img" aria-label="マイナンバーカード">
          <rect x="1" y="1" width="60" height="38" rx="5" fill="#fff" stroke="#bcd0ee" stroke-width="2"/>
          <rect x="7" y="8" width="20" height="14" rx="2" fill="#e7f0fd"/>
          <circle cx="17" cy="13.5" r="3.2" fill="#9bb8d6"/>
          <path d="M11 20c0-3 3-4 6-4s6 1 6 4" fill="#9bb8d6"/>
          <rect x="33" y="9" width="22" height="3" rx="1.5" fill="#0b5394"/>
          <rect x="33" y="16" width="22" height="3" rx="1.5" fill="#c7d6ea"/>
          <rect x="33" y="23" width="14" height="3" rx="1.5" fill="#c7d6ea"/>
          <rect x="7" y="28" width="30" height="6" rx="2" fill="#ffd66b"/>
        </svg>
        <span class="mynbr__t">${t("login.cardNote")}</span>
      </div>
      <form id="login-form" novalidate>
        <div class="field">
          <label for="serial">${t("login.cardLabel")}</label>
          <p class="hint">${t("login.cardHint")}</p>
          <input id="serial" name="serial" autocomplete="off" inputmode="latin" placeholder="JPKI-0001" required />
        </div>
        <div class="field">
          <label for="pin">${t("login.pinLabel")}</label>
          <p class="hint">${t("login.pinHint")}</p>
          <input id="pin" name="pin" class="pin-input" inputmode="numeric" pattern="[0-9]*" maxlength="4" placeholder="••••" required />
        </div>
        <p id="login-err" class="error-text" hidden></p>
        <button class="btn" type="submit">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M10 17l5-5-5-5" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M15 12H3" stroke="#fff" stroke-width="2" stroke-linecap="round"/><path d="M21 3v18" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
          ${t("common.login")}
        </button>
      </form>
      <p class="small muted" style="margin-top:14px">${t("login.demo")}</p>
    </section>
    <section class="card">
      <h2 class="section-title">${t("login.estTitle")}</h2>
      <p class="hint small">${t("login.estDesc")}</p>
      <button class="btn btn--outline" data-nav="estimate" type="button">${t("login.estBtn")}</button>
    </section>`;

  bindNav();
  $("#login-form").addEventListener("submit", async (e) => {
    e.preventDefault();
    const btn = $("#login-form button[type=submit]");
    const err = $("#login-err");
    err.hidden = true;
    btn.disabled = true; btn.textContent = t("common.loginProgress");
    try {
      const { token, citizen } = await api("/auth/card-login", {
        method: "POST",
        body: { cardSerial: $("#serial").value.trim(), pin: $("#pin").value.trim() },
      });
      setToken(token); state.me = citizen;
      toast(t("toast.welcome", { name: citizen.familyNameKanji }));
      go("home");
    } catch (ex) {
      err.textContent = ex.message; err.hidden = false;
      btn.disabled = false; btn.textContent = t("common.login");
    }
  });
}

// ── ホーム ──
async function renderHome() {
  setNav("home");
  const m = state.me;
  // よく使う手続き（過去の申請を種類別に集計）＋申請状況サマリー
  let freq = [];
  let statusRows = [];
  try {
    const [certs, procs] = await Promise.all([
      api("/certificate-requests").catch(() => []),
      api("/procedure-applications").catch(() => []),
    ]);
    const counts = new Map();
    const bump = (kind, typeId, name) => {
      const k = `${kind}:${typeId}`;
      const e = counts.get(k) || { kind, typeId, name, n: 0 };
      e.n++; counts.set(k, e);
    };
    for (const c of certs) bump("cert", c.typeId, c.typeName);
    for (const p of procs) bump("proc", p.typeId, p.typeName);
    freq = [...counts.values()].sort((a, b) => b.n - a.n).slice(0, 4);
    // 申請状況サマリー（状態別の件数）
    const all = [...certs, ...procs];
    const byStatus = {};
    for (const a of all) byStatus[a.status] = (byStatus[a.status] || 0) + 1;
    const STATUS_ORDER = [
      ["RECEIVED", "受付", "#1a73e8"], ["REVIEWING", "審査中", "#b26a00"],
      ["READY", "交付準備完了", "#1e7e34"], ["DELIVERED", "交付済", "#5b6573"],
      ["REJECTED", "却下", "#c0392b"], ["CANCELLED", "取消", "#9aa3ad"],
    ];
    statusRows = STATUS_ORDER.filter(([k]) => byStatus[k]).map(([k, label, color]) => ({ label: tStatus(k), color, value: byStatus[k] }));
  } catch {}
  const statusMax = Math.max(1, ...statusRows.map((r) => r.value));
  const statusHtml = statusRows.length ? `
    <section class="card">
      <h2 class="section-title">${t("home.summaryTitle")}</h2>
      <p class="small muted">${t("home.summaryDesc")}</p>
      <div class="hbars" data-nav="list" role="button" tabindex="0">
        ${statusRows.map((r) => `
          <div class="hbar-row">
            <span class="hbar-label">${esc(r.label)}</span>
            <span class="hbar-track"><span class="hbar-fill" style="width:${Math.round(r.value / statusMax * 100)}%;background:${r.color}"></span></span>
            <span class="hbar-value">${r.value}</span>
          </div>`).join("")}
      </div>
    </section>` : "";
  const freqHtml = freq.length ? `
    <section class="card">
      <h2 class="section-title">${t("home.freqTitle")}</h2>
      <p class="small muted">${t("home.freqDesc")}</p>
      <div class="list">
        ${freq.map((f) => `
          <button class="app-item" data-freq data-kind="${f.kind}" data-type="${esc(f.typeId)}" type="button">
            <span class="app-item__main">
              <span class="app-item__title">${esc(f.name)}</span>
              <span class="app-item__sub">${t("home.freqCount", { n: f.n })}</span>
            </span>
            <span aria-hidden="true">›</span>
          </button>`).join("")}
      </div>
    </section>` : "";
  view.innerHTML = `
    <section class="card hero">
      <h1>${t("home.greeting", { name: esc(m.familyNameKanji) + esc(m.givenNameKanji) })}</h1>
      <p>${t("home.greetingDesc")}</p>
    </section>
    <section class="card">
      <h2 class="section-title">${t("home.menuTitle")}</h2>
      <div class="grid-2">
        <button class="btn" data-nav="new" type="button">${t("home.do")}</button>
        <button class="btn btn--outline" data-nav="list" type="button">${t("home.check")}</button>
      </div>
      <p class="small muted" style="margin-top:10px">${t("home.menuDesc")}</p>
    </section>
    <section class="card">
      <h2 class="section-title">${t("home.estTitle")}</h2>
      <p class="small muted" style="margin-bottom:10px">${t("home.estDesc")}</p>
      <button class="btn btn--outline" data-nav="estimate" type="button" style="max-width:320px">${t("home.estBtn")}</button>
    </section>
    ${statusHtml}
    ${freqHtml}
    <section class="card">
      <h2 class="section-title">${t("home.infoTitle")}</h2>
      <p class="small muted">${t("home.infoDesc")}</p>
      <dl class="info">
        <div><dt class="muted small">${t("home.name")}</dt><dd>${esc(m.familyNameKanji)} ${esc(m.givenNameKanji)}（${esc(m.familyNameKana)} ${esc(m.givenNameKana)}）</dd></div>
        <div><dt class="muted small">${t("home.birth")}</dt><dd>${esc(m.birthDate)}</dd></div>
        <div><dt class="muted small">${t("home.address")}</dt><dd>${esc(m.addressText)}</dd></div>
      </dl>
    </section>`;
  bindNav();
  view.querySelectorAll("[data-freq]").forEach((b) => b.addEventListener("click", () => {
    if (b.dataset.kind === "cert") { state.prefill = { kind: "cert", typeId: b.dataset.type, data: {} }; go("cert"); }
    else go(`proc/${b.dataset.type}`);
  }));
}

// ── 手続きを選ぶ（ハブ）──
async function renderHub() {
  setNav("new");
  loading();
  let procs = [];
  try { procs = await api("/procedure-types"); } catch (ex) { toast(ex.message, true); }
  // カテゴリ別にグループ化（出現順を維持）
  const groups = [];
  const byCat = new Map();
  for (const p of procs) {
    const key = p.categoryLabel || "その他";
    if (!byCat.has(key)) { byCat.set(key, []); groups.push(key); }
    byCat.get(key).push(p);
  }
  const itemHtml = (go, title, sub) => `
    <button class="app-item" data-go="${go}" type="button">
      <span class="app-item__main">
        <span class="app-item__title">${esc(title)}</span>
        <span class="app-item__sub">${esc(sub || "")}</span>
      </span>
      <span aria-hidden="true">›</span>
    </button>`;
  view.innerHTML = `
    <section class="card">
      <h2 class="section-title">${t("form.hubTitle")}</h2>
      <p class="hint small">行いたい手続きを選んでください。いずれも来庁不要で、本人確認は完了しています。全${procs.length + 1}種類の手続きに対応しています。</p>
    </section>
    <section class="card">
      <h3 class="cat-title">証明書</h3>
      ${itemHtml("cert", "証明書のオンライン請求", "住民票・印鑑登録証明・課税証明・戸籍関係などをコンビニ交付／郵送で")}
    </section>
    ${groups.map((g) => `
      <section class="card">
        <h3 class="cat-title">${esc(g)}</h3>
        ${byCat.get(g).map((p) => itemHtml(`proc/${p.id}`, p.name, p.summary)).join("")}
      </section>`).join("")}`;
  view.querySelectorAll("[data-go]").forEach((b) => b.addEventListener("click", () => go(b.dataset.go)));
}

// ── 届出・申請手続き（転出届ほか）フォーム ──
async function renderProcForm(typeId) {
  setNav("new");
  loading();
  let types = [];
  try { types = await api("/procedure-types"); } catch (ex) { toast(ex.message, true); return go("new"); }
  const type = types.find((t) => t.id === typeId);
  if (!type) { toast("手続きが見つかりません。", true); return go("new"); }
  view.innerHTML = `
    <section class="card">
      <button class="btn btn--sm btn--outline" id="back" type="button" style="margin-bottom:12px">${t("form.backToList")}</button>
      <h2 class="section-title">${esc(type.name)}</h2>
      <p class="hint small">${esc(type.summary || "")}</p>
      <form id="proc-form">
        ${type.fields.map((f) => `
          <div class="field">
            <label for="f_${f.key}">${esc(f.label)}${f.required ? "" : "<span class=\"muted small\">（任意）</span>"}</label>
            ${f.type === "textarea"
              ? `<textarea id="f_${f.key}" name="${f.key}" ${f.required ? "required" : ""} placeholder="${esc(f.placeholder || "")}"></textarea>`
              : f.type === "select"
              ? `<select id="f_${f.key}" name="${f.key}" ${f.required ? "required" : ""}>${(f.options || []).map((o) => `<option value="${esc(o)}">${esc(o)}</option>`).join("")}</select>`
              : `<input id="f_${f.key}" name="${f.key}" type="${f.type === "tel" ? "tel" : f.type === "date" ? "date" : "text"}" ${f.required ? "required" : ""} placeholder="${esc(f.placeholder || "")}" ${f.type === "tel" ? 'inputmode="tel"' : ""} />`}
          </div>`).join("")}
        <p class="notice small">${t("form.applyNote")}</p>
        <p id="proc-err" class="error-text" hidden></p>
        <button class="btn btn--accent" type="submit">${t("form.applyBtn")}</button>
      </form>
    </section>`;
  $("#back").addEventListener("click", () => go("new"));
  const form = $("#proc-form");
  // 再申請: 前回の内容をプリフィル
  if (state.prefill && state.prefill.kind === "proc" && state.prefill.typeId === type.id) {
    const data = state.prefill.data || {}; state.prefill = null;
    for (const f of type.fields) {
      if (data[f.key] != null && form[f.key] != null) form[f.key].value = data[f.key];
    }
    toast("前回と同じ内容を入力しました。確認して申請してください。");
  } else {
    state.prefill = null; // 別手続きへ来たらプリフィルを破棄
  }
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const err = $("#proc-err"); err.hidden = true;
    const btn = $("#proc-form button[type=submit]");
    btn.disabled = true; btn.textContent = "送信中…";
    const payload = {};
    for (const f of type.fields) payload[f.key] = form[f.key].value.trim();
    try {
      const app = await api("/procedure-applications", { method: "POST", body: { typeId: type.id, payload } });
      toast("申請を受け付けました。");
      go(`pdetail/${app.id}`);
    } catch (ex) {
      err.textContent = ex.message; err.hidden = false;
      btn.disabled = false; btn.textContent = "この内容で申請する";
    }
  });
}

// ── 届出・申請手続きの詳細 ──
async function renderProcDetail(id) {
  setNav("list");
  loading();
  let a;
  try { a = await api(`/procedure-applications/${id}`); } catch (ex) { toast(ex.message, true); return go("list"); }
  const canCancel = !["DELIVERED", "CANCELLED", "REJECTED"].includes(a.status);
  const fieldRows = Object.entries(a.payload || {}).filter(([, v]) => v).map(([k, v]) =>
    `<div><dt class="muted small">${esc(k)}</dt><dd>${esc(v)}</dd></div>`).join("");
  view.innerHTML = `
    <section class="card">
      <button class="btn btn--sm btn--outline" id="back" type="button" style="margin-bottom:12px">${t("form.backShort")}</button>
      <div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
        <h2 class="section-title" style="margin:0">${esc(a.typeName)}</h2>
        <span class="badge badge--${a.status}">${tStatus(a.status)}</span>
      </div>
      <dl class="info" style="margin-top:12px">
        <div><dt class="muted small">${t("list.receipt")}</dt><dd>${esc(a.receiptNumber)}</dd></div>
        ${fieldRows}
        <div><dt class="muted small">${t("detail.submittedAt")}</dt><dd>${new Date(a.submittedAt).toLocaleString("ja-JP")}</dd></div>
      </dl>
    </section>
    <section class="card">
      <h2 class="section-title">${t("detail.status")}</h2>
      <ul class="timeline">
        ${a.history.map((h) => `<li><strong>${tStatus(h.status)}</strong><br><span class="small muted">${new Date(h.at).toLocaleString("ja-JP")}</span></li>`).join("")}
      </ul>
      <p class="small muted">この申請は担当課（${esc(a.routedTo)}）へ連携されています（連携状況: ${esc(a.linkStatus)}）。</p>
      <div class="btn-row" style="margin-top:14px">
        <button class="btn btn--sm btn--outline" id="refresh" type="button">${t("detail.refresh")}</button>
        <button class="btn btn--sm btn--outline" id="reapply" type="button">${t("detail.reapply")}</button>
        <button class="btn btn--sm btn--outline" id="receipt-btn" type="button">${t("detail.print")}</button>
        ${canCancel ? `<button class="btn btn--sm btn--danger" id="cancel" type="button">${t("detail.cancel")}</button>` : ""}
      </div>
    </section>`;
  $("#back").addEventListener("click", () => go("list"));
  $("#refresh").addEventListener("click", () => render());
  $("#receipt-btn").addEventListener("click", () => openReceipt(a));
  $("#reapply").addEventListener("click", () => {
    state.prefill = { kind: "proc", typeId: a.typeId, data: { ...a.payload } };
    go(`proc/${a.typeId}`);
  });
  const cancelBtn = $("#cancel");
  if (cancelBtn) cancelBtn.addEventListener("click", async () => {
    if (!confirm("この申請を取消しますか？")) return;
    try { await api(`/procedure-applications/${id}/cancel`, { method: "POST" }); toast("取消しました。"); render(); }
    catch (ex) { toast(ex.message, true); }
  });
}

// ── 新規申請（証明書オンライン請求）──
async function renderNew() {
  setNav("new");
  loading();
  if (!state.types.length) state.types = await api("/certificate-types");
  const types = state.types;
  view.innerHTML = `
    <section class="card">
      <button class="btn btn--sm btn--outline" id="cert-back" type="button" style="margin-bottom:12px">${t("form.backToList")}</button>
      <h2 class="section-title">${t("cert.title")}</h2>
      <p class="hint small">請求する証明書・部数・受取方法を選んでください。手数料はあとで確認できます。</p>
      <form id="req-form">
        <fieldset class="field" style="border:0;padding:0;margin:0 0 8px">
          <legend style="font-weight:700;margin-bottom:8px">${t("cert.step1")}</legend>
          ${types.map((t, i) => `
            <label class="choice">
              <input type="radio" name="typeId" value="${t.id}" ${i === 0 ? "checked" : ""} />
              <span class="choice__name">${esc(t.name)}</span>
              <span class="choice__meta">１部 ${yen(t.unitFee)}${t.note ? "・" + esc(t.note) : ""}</span>
            </label>`).join("")}
        </fieldset>
        <div class="field">
          <label for="copies">② 部数</label>
          <select id="copies" name="copies">${[1,2,3,4,5].map((n) => `<option value="${n}">${n}部</option>`).join("")}</select>
        </div>
        <fieldset class="field" id="delivery-field" style="border:0;padding:0;margin:0 0 8px">
          <legend style="font-weight:700;margin-bottom:8px">${t("cert.step3")}</legend>
          <div id="delivery-options"></div>
        </fieldset>
        <div class="field">
          <label for="usage">④ 使いみち（任意）</label>
          <input id="usage" name="usage" maxlength="200" placeholder="例: 年金手続き、提出先など" />
        </div>
        <div class="card" style="background:#f8fafc">
          <div class="totals"><span>お支払い手数料</span><span id="fee-total">—</span></div>
          <p class="small muted" id="fee-note"></p>
        </div>
        <button class="btn btn--accent" type="submit">${t("cert.requestBtn")}</button>
      </form>
    </section>`;

  $("#cert-back").addEventListener("click", () => go("new"));
  const form = $("#req-form");
  const typeOf = () => types.find((t) => t.id === form.typeId.value);

  function renderDelivery() {
    const t = typeOf();
    $("#delivery-options").innerHTML = t.delivery.map((d, i) => `
      <label class="choice">
        <input type="radio" name="deliveryMethod" value="${d}" ${i === 0 ? "checked" : ""} />
        <span class="choice__name">${tDelivery(d)}</span>
        <span class="choice__meta">${d === "POSTAL" ? "送料140円が加算されます（数日でお届け）" : "お近くのコンビニ等で受け取り（即時）"}</span>
      </label>`).join("");
    updateFee();
  }
  function updateFee() {
    const t = typeOf();
    const copies = Number(form.copies.value);
    const delivery = form.deliveryMethod ? form.deliveryMethod.value : t.delivery[0];
    const fee = t.unitFee * copies + (delivery === "POSTAL" ? 140 : 0);
    $("#fee-total").textContent = yen(fee);
    $("#fee-note").textContent = `${esc(t.name)} × ${copies}部` + (delivery === "POSTAL" ? "（＋送料140円）" : "");
  }

  form.addEventListener("change", (e) => {
    if (e.target.name === "typeId") renderDelivery(); else updateFee();
  });
  renderDelivery();

  // 再申請: 前回の内容をプリフィル
  if (state.prefill && state.prefill.kind === "cert") {
    const p = state.prefill; state.prefill = null;
    const typeRadio = form.querySelector(`input[name="typeId"][value="${p.typeId}"]`);
    if (typeRadio) { typeRadio.checked = true; renderDelivery(); }
    if (p.data.copies) form.copies.value = String(p.data.copies);
    const delRadio = form.querySelector(`input[name="deliveryMethod"][value="${p.data.deliveryMethod}"]`);
    if (delRadio) delRadio.checked = true;
    if (p.data.usage) $("#usage").value = p.data.usage;
    updateFee();
    toast("前回と同じ内容を入力しました。確認して申請してください。");
  }

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const btn = $("#req-form button[type=submit]");
    btn.disabled = true; btn.textContent = "送信中…";
    try {
      const app = await api("/certificate-requests", {
        method: "POST",
        body: {
          typeId: form.typeId.value,
          copies: Number(form.copies.value),
          deliveryMethod: form.deliveryMethod.value,
          usage: $("#usage").value.trim(),
        },
      });
      toast("申請を受け付けました。");
      go(`detail/${app.id}`);
    } catch (ex) {
      toast(ex.message, true);
      btn.disabled = false; btn.textContent = "この内容で請求する";
    }
  });
}

// ── 申請一覧 ──
let _listFilter = "ALL";
let _listSearch = "";
async function renderList() {
  setNav("list");
  loading();
  let certs = [], procs = [];
  try {
    [certs, procs] = await Promise.all([
      api("/certificate-requests").catch(() => []),
      api("/procedure-applications").catch(() => []),
    ]);
  } catch (ex) { toast(ex.message, true); }
  const items = [
    ...certs.map((a) => ({ ...a, _kind: "cert", _sub: `${t("list.receipt")} ${a.receiptNumber}・${tDelivery(a.deliveryMethod)}・${yen(a.fee)}`, _title: `${a.typeName} ${a.copies}部` })),
    ...procs.map((a) => ({ ...a, _kind: "proc", _sub: `${t("list.receipt")} ${a.receiptNumber}`, _title: a.typeName })),
  ].sort((x, y) => new Date(y.submittedAt) - new Date(x.submittedAt));

  if (!items.length) {
    view.innerHTML = `
      <section class="card center">
        <h2 class="section-title">${t("nav.list")}</h2>
        <p class="muted">${t("list.empty")}</p>
        <button class="btn" data-nav="new" type="button" style="max-width:320px;margin:8px auto 0">${t("home.do")}</button>
      </section>`;
    bindNav();
    return;
  }
  const statuses = [...new Set(items.map((i) => i.status))];
  if (_listFilter !== "ALL" && !statuses.includes(_listFilter)) _listFilter = "ALL";
  const chip = (val, label, n) => `<button class="chip ${_listFilter === val ? "chip--on" : ""}" data-filter="${val}" type="button">${esc(label)}（${n}）</button>`;
  view.innerHTML = `
    <section class="card">
      <h2 class="section-title">${t("list.title", { n: items.length })}</h2>
      <input type="search" id="list-search" class="list-search" placeholder="${t("list.search")}" aria-label="申請をキーワード検索" value="${esc(_listSearch)}" />
      <div class="chips" role="group" aria-label="状態でしぼり込み">
        ${chip("ALL", t("list.all"), items.length)}
        ${statuses.map((s) => chip(s, tStatus(s), items.filter((i) => i.status === s).length)).join("")}
      </div>
      ${items.map((a) => `
        <button class="app-item" data-route="${a._kind === "cert" ? "detail" : "pdetail"}/${a.id}" data-status="${a.status}" data-search="${esc((a._title + " " + a._sub).toLowerCase())}" type="button">
          <span class="app-item__main">
            <span class="app-item__title">${esc(a._title)}</span>
            <span class="app-item__sub">${esc(a._sub)}</span>
          </span>
          <span class="badge badge--${a.status}">${tStatus(a.status)}</span>
        </button>`).join("")}
      <p id="list-empty" class="muted small center" hidden>${t("list.noMatch")}</p>
    </section>`;
  const applyFilter = () => {
    const kw = _listSearch.trim().toLowerCase();
    view.querySelectorAll("[data-filter]").forEach((c) => c.classList.toggle("chip--on", c.dataset.filter === _listFilter));
    view.querySelectorAll(".app-item").forEach((it) => {
      const statusOk = _listFilter === "ALL" || it.dataset.status === _listFilter;
      const kwOk = !kw || (it.dataset.search || "").includes(kw);
      it.hidden = !(statusOk && kwOk);
    });
    const anyVisible = [...view.querySelectorAll(".app-item")].some((it) => !it.hidden);
    document.getElementById("list-empty").hidden = anyVisible;
  };
  const searchInput = document.getElementById("list-search");
  searchInput.addEventListener("input", () => { _listSearch = searchInput.value; applyFilter(); });
  view.querySelectorAll("[data-filter]").forEach((b) => b.addEventListener("click", () => { _listFilter = b.dataset.filter; applyFilter(); }));
  view.querySelectorAll(".app-item").forEach((b) => b.addEventListener("click", () => go(b.dataset.route)));
  applyFilter();
}

// ── 申請詳細 ──
async function renderDetail(id) {
  setNav("list");
  loading();
  let a;
  try { a = await api(`/certificate-requests/${id}`); } catch (ex) { toast(ex.message, true); return go("list"); }
  const canCancel = !["DELIVERED", "CANCELLED", "REJECTED"].includes(a.status);
  view.innerHTML = `
    <section class="card">
      <button class="btn btn--sm btn--outline" id="back" type="button" style="margin-bottom:12px">${t("form.backShort")}</button>
      <div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
        <h2 class="section-title" style="margin:0">${esc(a.typeName)}</h2>
        <span class="badge badge--${a.status}">${tStatus(a.status)}</span>
      </div>
      <dl class="info" style="margin-top:12px">
        <div><dt class="muted small">${t("list.receipt")}</dt><dd>${esc(a.receiptNumber)}</dd></div>
        <div><dt class="muted small">${t("detail.copies")}</dt><dd>${a.copies}部</dd></div>
        <div><dt class="muted small">${t("detail.delivery")}</dt><dd>${tDelivery(a.deliveryMethod)}</dd></div>
        <div><dt class="muted small">${t("detail.fee")}</dt><dd>${yen(a.fee)}</dd></div>
        ${a.usage ? `<div><dt class="muted small">${t("detail.usage")}</dt><dd>${esc(a.usage)}</dd></div>` : ""}
        <div><dt class="muted small">${t("detail.submittedAt")}</dt><dd>${new Date(a.submittedAt).toLocaleString("ja-JP")}</dd></div>
      </dl>
    </section>
    <section class="card">
      <h2 class="section-title">${t("detail.status")}</h2>
      <ul class="timeline">
        ${a.history.map((h) => `<li><strong>${tStatus(h.status)}</strong><br><span class="small muted">${new Date(h.at).toLocaleString("ja-JP")}</span></li>`).join("")}
      </ul>
      <p class="small muted">この申請は担当課（${esc(a.routedTo)}）へ連携されています（連携状況: ${esc(a.linkStatus)}）。</p>
      ${a.status === "READY" ? `<p class="notice">交付準備が整いました。${a.deliveryMethod === "CONVENIENCE" ? "コンビニ等で受け取れます。" : "郵送でお届けします。"}</p>` : ""}
      <div class="btn-row" style="margin-top:14px">
        <button class="btn btn--sm btn--outline" id="refresh" type="button">${t("detail.refresh")}</button>
        <button class="btn btn--sm btn--outline" id="reapply" type="button">${t("detail.reapply")}</button>
        <button class="btn btn--sm btn--outline" id="receipt-btn" type="button">${t("detail.print")}</button>
        ${canCancel ? `<button class="btn btn--sm btn--danger" id="cancel" type="button">${t("detail.cancel")}</button>` : ""}
      </div>
    </section>`;
  $("#back").addEventListener("click", () => go("list"));
  $("#refresh").addEventListener("click", () => render());
  $("#receipt-btn").addEventListener("click", () => openReceipt(a));
  $("#reapply").addEventListener("click", () => {
    state.prefill = { kind: "cert", typeId: a.typeId, data: { copies: a.copies, deliveryMethod: a.deliveryMethod, usage: a.usage || "" } };
    go("cert");
  });
  const cancelBtn = $("#cancel");
  if (cancelBtn) cancelBtn.addEventListener("click", async () => {
    if (!confirm("この申請を取消しますか？")) return;
    try { await api(`/certificate-requests/${id}/cancel`, { method: "POST" }); toast("取消しました。"); render(); }
    catch (ex) { toast(ex.message, true); }
  });
}

// ── 制度の試算（クイック見積もり）ハブ ──
async function renderEstimateHub() {
  setNav("estimate");
  loading();
  let types = [];
  try { types = await api("/estimate-types"); } catch (ex) { toast(ex.message, true); }
  const groups = [];
  const byCat = new Map();
  for (const t of types) {
    const key = t.categoryLabel || "その他";
    if (!byCat.has(key)) { byCat.set(key, []); groups.push(key); }
    byCat.get(key).push(t);
  }
  const item = (t) => `
    <button class="app-item" data-go="estimate/${esc(t.id)}" type="button">
      <span class="app-item__main">
        <span class="app-item__title">${esc(t.name)}</span>
        <span class="app-item__sub">${esc(t.summary || "")}</span>
      </span>
      <span aria-hidden="true">›</span>
    </button>`;
  view.innerHTML = `
    <section class="card">
      <h2 class="section-title">${t("est.hubTitle")}</h2>
      <p class="hint small">${t("est.hubDesc")}</p>
    </section>
    ${groups.map((g) => `
      <section class="card">
        <h3 class="cat-title">${esc(g)}</h3>
        ${byCat.get(g).map(item).join("")}
      </section>`).join("")}
    ${state.me ? "" : `<section class="card"><p class="small muted">${t("est.loginNote")}</p><button class="btn btn--outline" data-nav="login" type="button">${t("est.loginBtn")}</button></section>`}`;
  view.querySelectorAll("[data-go]").forEach((b) => b.addEventListener("click", () => go(b.dataset.go)));
  bindNav();
}

// ── 試算フォーム（入力→即結果）──
async function renderEstimateForm(id) {
  setNav("estimate");
  loading();
  let types = [];
  try { types = await api("/estimate-types"); } catch (ex) { toast(ex.message, true); return go("estimate"); }
  const et = types.find((x) => x.id === id);
  if (!et) { toast("試算メニューが見つかりません。", true); return go("estimate"); }
  const fieldHtml = (f) => {
    const req = f.required ? "required" : "";
    const opt = f.required ? "" : '<span class="muted small">（任意）</span>';
    let control;
    if (f.type === "select") {
      control = `<select id="e_${f.key}" name="${f.key}" ${req}>${(f.options || []).map((o) => `<option value="${esc(o.value)}">${esc(o.label)}</option>`).join("")}</select>`;
    } else if (f.type === "members" || f.type === "dates") {
      control = `<textarea id="e_${f.key}" name="${f.key}" rows="3" ${req} placeholder="${esc(f.placeholder || "")}"></textarea>`;
    } else if (f.type === "number") {
      control = `<input id="e_${f.key}" name="${f.key}" type="number" inputmode="numeric" ${req} placeholder="${esc(f.placeholder || "")}" />`;
    } else {
      control = `<input id="e_${f.key}" name="${f.key}" type="text" ${req} placeholder="${esc(f.placeholder || "")}" />`;
    }
    return `<div class="field"><label for="e_${f.key}">${esc(f.label)}${opt}</label>${control}</div>`;
  };
  view.innerHTML = `
    <section class="card">
      <button class="btn btn--sm btn--outline" id="est-back" type="button" style="margin-bottom:12px">${t("est.backToMenu")}</button>
      <h2 class="section-title">${esc(et.name)}の試算</h2>
      <p class="hint small">${esc(et.summary || "")}</p>
      <form id="est-form">
        ${et.fields.map(fieldHtml).join("")}
        <p id="est-err" class="error-text" hidden></p>
        <button class="btn btn--accent" type="submit">${t("est.formBtn")}</button>
      </form>
      <div id="est-result" hidden></div>
      <p class="small muted" style="margin-top:12px">※ 標準的なパラメータ（令和6年度）による目安です。実際の額は自治体の条例・個別事情により異なります。</p>
    </section>`;
  $("#est-back").addEventListener("click", () => go("estimate"));
  const form = $("#est-form");
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const err = $("#est-err"); err.hidden = true;
    const btn = form.querySelector("button[type=submit]");
    btn.disabled = true; btn.textContent = "計算中…";
    const input = {};
    for (const f of et.fields) input[f.key] = form[f.key] ? form[f.key].value : "";
    try {
      const r = await api("/estimate", { method: "POST", body: { id: et.id, input } });
      renderEstimateResult(r);
    } catch (ex) {
      err.textContent = ex.message; err.hidden = false;
    } finally {
      btn.disabled = false; btn.textContent = "再計算する";
    }
  });
}

function renderEstimateResult(r) {
  const box = $("#est-result");
  if (!box) return;
  const p = r.primary || {};
  const main = p.format === "yenMonthly" ? `${yen(p.value)}<span class="estimate-result__unit"> / 月</span>`
    : p.format === "yen" ? yen(p.value)
    : esc(p.value);
  const rows = (r.details || []).map(([k, v]) => `<div><dt class="muted small">${esc(k)}</dt><dd>${esc(v)}</dd></div>`).join("");
  // 試算→そのまま申請の導線（関連する手続きへ）
  const apply = r.apply ? `
    <div class="estimate-cta">
      <p class="small muted">この試算に関連する手続きは、来庁不要でオンライン申請できます。</p>
      <button class="btn btn--accent" id="est-apply" type="button">${esc(r.apply.label)} →</button>
    </div>` : "";
  box.hidden = false;
  box.innerHTML = `
    <div class="estimate-result" role="status">
      <span class="estimate-result__label">${esc(p.label || "試算結果")}</span>
      <span class="estimate-result__value">${main}</span>
    </div>
    ${rows ? `<dl class="info estimate-detail">${rows}</dl>` : ""}
    ${apply}`;
  const applyBtn = $("#est-apply");
  if (applyBtn) applyBtn.addEventListener("click", () => applyFromEstimate(r));
  box.scrollIntoView({ behavior: "smooth", block: "nearest" });
}

// 試算結果から関連する申請フォームへ遷移（入力を可能な範囲でプリフィル）
async function applyFromEstimate(r) {
  const ap = r.apply;
  if (!ap) return;
  if (!state.me) {
    toast("申請にはログインが必要です。ログイン後にもう一度お試しください。");
    return go("login");
  }
  if (ap.kind === "cert") {
    if (!state.types.length) { try { state.types = await api("/certificate-types"); } catch {} }
    const certName = r.echo && r.echo.certType;
    const t = state.types.find((x) => x.name === certName);
    if (!t) { toast("対応する証明書が見つかりません。", true); return go("cert"); }
    const copies = Math.max(1, Math.min(5, Number(r.echo && r.echo.copies) || 1));
    const deliveryMethod = r.echo && r.echo.postal ? "POSTAL" : ((t.delivery && t.delivery[0]) || "CONVENIENCE");
    state.prefill = { kind: "cert", typeId: t.id, data: { copies, deliveryMethod } };
    return go("cert");
  }
  // proc: 可能ならフォームの項目をプリフィル
  const data = {};
  if (r.id === "child-allowance" && r.echo && r.echo.births) {
    const list = String(r.echo.births).split(/\n+/).map((s) => s.trim()).filter(Boolean).sort();
    if (list.length) data.childBirthDate = list[list.length - 1]; // 最年少（最新の生年月日）
  }
  state.prefill = { kind: "proc", typeId: ap.typeId, data };
  return go(`proc/${ap.typeId}`);
}

// ── お知らせ（申請状況の通知）──
async function renderNotifications() {
  setNav("");
  loading();
  let data = { unread: 0, items: [] };
  try { data = await api("/notifications"); } catch (ex) { toast(ex.message, true); }
  const items = data.items || [];
  // 未読を既読化してバッジを更新
  if (data.unread > 0) { try { await api("/notifications/read", { method: "POST" }); } catch {} }
  view.innerHTML = `
    <section class="card">
      <h2 class="section-title">${t("notif.title")}</h2>
      <p class="hint small">${t("notif.desc")}</p>
    </section>
    ${items.length === 0
      ? `<section class="card"><p class="muted">${t("notif.empty")}</p></section>`
      : `<section class="card"><ul class="notif-list">${items.map((n) => `
          <li class="notif-item ${n.read ? "" : "notif-item--unread"}" data-app="${esc(n.applicationId)}" data-kind="${esc(n.kind || "cert")}">
            <div class="notif-item__top">
              <span class="badge badge--${n.status}">${esc(tStatus(n.status))}</span>
              <span class="small muted">${new Date(n.createdAt).toLocaleString("ja-JP")}</span>
            </div>
            <p class="notif-item__title">${esc(n.typeName)}</p>
            <p class="notif-item__msg small">${esc(t("notif.msg." + n.status) || n.message)}</p>
            ${n.receiptNumber ? `<p class="small muted">${t("notif.receipt")}: ${esc(n.receiptNumber)}</p>` : ""}
          </li>`).join("")}</ul></section>`}`;
  view.querySelectorAll(".notif-item").forEach((li) =>
    li.addEventListener("click", () => go(`${li.dataset.kind === "proc" ? "pdetail" : "detail"}/${li.dataset.app}`)));
  updateNotifBadge();
}

// 未読件数バッジを更新（ログイン中のみ）
async function updateNotifBadge() {
  const badge = $("#notif-badge");
  if (!badge) return;
  if (!state.me) { badge.hidden = true; return; }
  try {
    const data = await api("/notifications");
    const n = data.unread || 0;
    badge.textContent = n > 99 ? "99+" : String(n);
    badge.hidden = n === 0;
  } catch { badge.hidden = true; }
}

function bindNav() {
  view.querySelectorAll("[data-nav]").forEach((b) => b.addEventListener("click", () => go(b.dataset.nav)));
}

// ── グローバルなナビ要素 ──
document.querySelectorAll(".tabbar__item").forEach((b) => b.addEventListener("click", () => go(b.dataset.nav)));
$("#home-btn").addEventListener("click", () => go(state.me ? "home" : "login"));
$("#notif-btn").addEventListener("click", () => go("notifications"));
$("#logout-btn").addEventListener("click", async () => {
  try { await api("/auth/logout", { method: "POST" }); } catch {}
  setToken(null); state.me = null;
  const badge = $("#notif-badge"); if (badge) badge.hidden = true;
  toast(t("toast.loggedOut"));
  go("login");
});

// 申請状況の通知バッジを定期更新（簡易リアルタイム）
setInterval(() => { if (state.me) updateNotifBadge(); }, 30000);

// 言語切替（日本語 / English / やさしい日本語）
$("#lang-select")?.addEventListener("change", (e) => setLang(e.target.value));

// PWA: service worker 登録
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => navigator.serviceWorker.register("/sw.js").catch(() => {}));
}

applyStaticI18n();
render();
