/**
 * TaskMate admin panel — sidebar entry at /taskmate-admin.
 *
 * HA-native styling:
 *   - All custom --tm-* tokens now map to HA theme variables.
 *   - Dark mode handled automatically via HA vars.
 *   - Version extracted from resource URL (?v=x.x.x), same as cards.
 */

const PANEL_VERSION = (() => {
  // The version comes from the resource URL (?v=x.x.x), set by the integration
  // itself — but it is still DOM-sourced text that we later splice into
  // innerHTML, so clamp it to a version-safe charset to remove any chance of
  // HTML injection (and silence CodeQL js/xss-through-dom).
  const sanitize = (v) => ((v || "").replace(/[^\w.+-]/g, "") || "?");
  try { return sanitize(new URL(import.meta.url).searchParams.get("v")); } catch (_) {}
  const el = document.querySelector('script[src*="/taskmate-panel.js"]');
  return el ? sanitize(new URLSearchParams(el.src.split("?")[1] || "").get("v")) : "?";
})();

const BADGE_METRICS = [
  { v: "total_points",  lk: "badge.metric_total_points" },
  { v: "total_chores",  lk: "badge.metric_total_chores" },
  { v: "total_rewards", lk: "badge.metric_total_rewards" },
  { v: "current_streak", lk: "badge.metric_current_streak" },
  { v: "best_streak",   lk: "badge.metric_best_streak" },
  { v: "perfect_weeks", lk: "badge.metric_perfect_weeks" },
  { v: "first_chore",   lk: "badge.metric_first_chore" },
  { v: "first_reward",  lk: "badge.metric_first_reward" },
];
const BADGE_BOOL_METRICS = ["first_chore", "first_reward"];
const BADGE_OPERATORS = ["≥", "=", "≤", ">", "<", "≠"];
const BADGE_OP_VALUES = { "≥": ">=", "=": "==", "≤": "<=", ">": ">", "<": "<", "≠": "!=" };
const BADGE_TIERS = [
  { v: "bronze",   lk: "badge.tier_bronze" },
  { v: "silver",   lk: "badge.tier_silver" },
  { v: "gold",     lk: "badge.tier_gold" },
  { v: "platinum", lk: "badge.tier_platinum" },
];

const SCHEDULE_MODES = [
  { v: "specific_days", lk: "panel.schedule_specific_days" },
  { v: "recurring",     lk: "panel.schedule_recurring" },
  { v: "one_shot",      lk: "panel.schedule_one_shot" },
];

const RECURRENCES = [
  { v: "every_2_days",   lk: "panel.recurrence_every_2_days" },
  { v: "weekly",         lk: "panel.recurrence_weekly" },
  { v: "every_2_weeks",  lk: "panel.recurrence_every_2_weeks" },
  { v: "monthly",        lk: "panel.recurrence_monthly" },
  { v: "every_3_months", lk: "panel.recurrence_every_3_months" },
  { v: "every_6_months", lk: "panel.recurrence_every_6_months" },
];

const FIRST_OCCURRENCE = [
  { v: "available_immediately",     lk: "panel.first_occ_available_immediately" },
  { v: "wait_for_first_occurrence", lk: "panel.first_occ_wait" },
];

const ASSIGNMENT_MODES = [
  { v: "everyone",    lk: "panel.assign_everyone" },
  { v: "alternating", lk: "panel.assign_alternating" },
  { v: "random",      lk: "panel.assign_random" },
  { v: "balanced",    lk: "panel.assign_balanced" },
  { v: "first_come",  lk: "panel.assign_first_come" },
  { v: "unassigned",  lk: "panel.assign_unassigned" },
];

const VISIBILITY_OPS = [
  { v: "none",       lk: "panel.vis_none" },
  { v: "equals",     lk: "panel.vis_equals" },
  { v: "not_equals", lk: "panel.vis_not_equals" },
  { v: "gte",        lk: "panel.vis_gte" },
  { v: "lte",        lk: "panel.vis_lte" },
  { v: "gt",         lk: "panel.vis_gt" },
  { v: "lt",         lk: "panel.vis_lt" },
];

// Chore fields a change can be scheduled against (#675). Must stay in step with
// SCHEDULED_CHANGE_FIELDS in models.py — the backend rejects anything else.
const SCHEDULED_FIELDS = [
  { v: "points",                   kind: "number" },
  { v: "assigned_to",              kind: "children" },
  { v: "enabled",                  kind: "bool" },
  { v: "requires_approval",        kind: "bool" },
  { v: "daily_limit",              kind: "number" },
  { v: "due_days",                 kind: "days" },
  { v: "mandatory",                kind: "bool" },
  { v: "mandatory_penalty_points", kind: "number" },
  { v: "expires_on",               kind: "text" },
  { v: "description",              kind: "text" },
  { v: "time_category", kind: "select", options: [
    { v: "anytime",   lk: "panel.time_anytime" },
    { v: "morning",   lk: "panel.time_morning" },
    { v: "afternoon", lk: "panel.time_afternoon" },
    { v: "evening",   lk: "panel.time_evening" },
    { v: "night",     lk: "panel.time_night" },
  ] },
  { v: "difficulty", kind: "select", options: [
    { v: "easy",   lk: "panel.difficulty_easy" },
    { v: "medium", lk: "panel.difficulty_medium" },
    { v: "hard",   lk: "panel.difficulty_hard" },
  ] },
];

// Adverse HA weather conditions a chore can be blocked by. The pleasant ones
// (sunny, clear-night, partlycloudy) are deliberately omitted — nobody rains
// off "mow the lawn" because it's sunny.
const WEATHER_CONDITIONS = [
  { v: "rainy",          icon: "mdi:weather-rainy" },
  { v: "pouring",        icon: "mdi:weather-pouring" },
  { v: "snowy",          icon: "mdi:weather-snowy" },
  { v: "snowy-rainy",    icon: "mdi:weather-snowy-rainy" },
  { v: "hail",           icon: "mdi:weather-hail" },
  { v: "lightning",      icon: "mdi:weather-lightning" },
  { v: "lightning-rainy", icon: "mdi:weather-lightning-rainy" },
  { v: "fog",            icon: "mdi:weather-fog" },
  { v: "windy",          icon: "mdi:weather-windy" },
  { v: "windy-variant",  icon: "mdi:weather-windy-variant" },
  { v: "cloudy",         icon: "mdi:weather-cloudy" },
  { v: "exceptional",    icon: "mdi:alert-outline" },
];

const COMPLETION_SOUNDS = [
  "none", "coin", "levelup", "fanfare", "chime", "powerup", "undo",
  "fart1", "fart2", "fart3", "fart4", "fart5", "fart6", "fart7",
  "fart8", "fart9", "fart10", "fart_random",
];

const DAYS = [
  { v: "monday",    lk: "panel.day_mon" },
  { v: "tuesday",   lk: "panel.day_tue" },
  { v: "wednesday", lk: "panel.day_wed" },
  { v: "thursday",  lk: "panel.day_thu" },
  { v: "friday",    lk: "panel.day_fri" },
  { v: "saturday",  lk: "panel.day_sat" },
  { v: "sunday",    lk: "panel.day_sun" },
];

const STREAK_MODES = [
  { v: "reset", lk: "panel.streak_reset" },
  { v: "pause", lk: "panel.streak_pause" },
];

const TASK_GROUP_POLICIES = [
  { v: "sticky", lk: "panel.group_policy_sticky" },
  { v: "spread", lk: "panel.group_policy_spread" },
];


class TaskMatePanel extends HTMLElement {
  constructor() {
    super();
    this._hass = null;
    this._state = null;
    this._error = null;
    this._loading = false;
    this._timePeriodsDraft = null;  // local edit state for the period editor
    this._vacationDraft = null;     // local edit state for the vacation editor
    this._bulkMode = false;         // chores multi-select mode
    this._bulkSel = new Set();      // selected chore ids for bulk actions
    this._activeTab = "children";
    this._mobileNavOpen = false;     // mobile section-picker dropdown state
    this._dialog = null;
    this._dialogInitialHash = null;  // for confirm-on-leave
    this._filter = "";               // per-tab search/filter
    this._inlineRename = null;       // { kind: "chore", id, value }
    this._rowMenuEl = null;          // floating row-action menu element (chores)
    this._rowMenuForId = null;       // chore id the open row menu belongs to
    this._onRowMenuDismiss = null;   // scroll/resize listener while menu open
    this._reorderDrag = null;        // tracks drag state during reorder
    this._templateView = null;       // null | "picker" | "preview"
    this._templateSelected = null;   // selected template object
    this._templateChores = [];       // editable chores for preview step
    this._saveTemplateDialog = false; // "save as template" dialog state
    this._badgesSubTab = "catalogue"; // "catalogue" | "custom" | "history"
    this._rendered = false;
    this._shellReady = false;
    this._zoneCache = {};
    this._cachedStyles = null;
    this._onFocusIn = this._onFocusIn.bind(this);
    this._onClick = this._onClick.bind(this);
    this._onDblClick = this._onDblClick.bind(this);
    this._onInput = this._onInput.bind(this);
    this._onChange = this._onChange.bind(this);
    this._onValueChanged = this._onValueChanged.bind(this);
    this._onKeyDown = this._onKeyDown.bind(this);
    this._onDragStart = this._onDragStart.bind(this);
    this._onDragOver = this._onDragOver.bind(this);
    this._onDrop = this._onDrop.bind(this);
    this._onVisibilityChange = this._onVisibilityChange.bind(this);
    this._lastConnected = null;
    this._showIds = localStorage.getItem("taskmate-show-ids") === "true";
    this._ensureHaComponents();
  }

  async _ensureHaComponents() {
    if (customElements.get("ha-textfield")) return;
    try { await window.loadCardHelpers?.(); } catch (_) {}
    await Promise.all([
      customElements.whenDefined("ha-textfield").catch(() => {}),
      customElements.whenDefined("ha-switch").catch(() => {}),
      customElements.whenDefined("ha-icon-picker").catch(() => {}),
    ]);
    if (this._rendered) this._render();
  }

  set hass(value) {
    const first = this._hass === null;
    const prevConnected = this._lastConnected;
    this._hass = value;

    const isDark = this._isHaDark();
    this.classList.toggle("dark", isDark);
    this.classList.toggle("light", !isDark);
    const connNow = !!(value && value.connection && value.connection.connected !== false);
    this._lastConnected = connNow;

    if (first) {
      this._fetchState();
    } else if (connNow && prevConnected === false) {
      // WS reconnected after a drop — refetch so the panel reflects current state.
      this._fetchState();
    } else if (connNow && this._error) {
      // Recover from a previous fetch failure (e.g. WS was mid-handshake earlier).
      this._error = null;
      this._fetchState();
    }
    // Render on first hass, or if HA pushed a fresh hass after a reconnect
    // that completed while the tab was already visible (no visibilitychange
    // to lean on) and our shell was stripped out in the meantime.
    if (!this._rendered || !this.querySelector(".tm-shell")) this._render();
    this._bindHaPickers(false);
  }
  get hass() { return this._hass; }
  set narrow(_v) {}
  set route(_v) {}
  set panel(_v) {}

  connectedCallback() {
    this.addEventListener("focusin", this._onFocusIn);
    this.addEventListener("click", this._onClick);
    this.addEventListener("dblclick", this._onDblClick);
    this.addEventListener("input", this._onInput);
    this.addEventListener("change", this._onChange);
    this.addEventListener("value-changed", this._onValueChanged);
    this.addEventListener("keydown", this._onKeyDown);
    this.addEventListener("dragstart", this._onDragStart);
    this.addEventListener("dragover", this._onDragOver);
    this.addEventListener("drop", this._onDrop);
    document.addEventListener("visibilitychange", this._onVisibilityChange);
    if (!this.classList.contains("dark") && !this.classList.contains("light")) {
      const isDark = this._isHaDark();
      this.classList.toggle("dark", isDark);
      this.classList.toggle("light", !isDark);
    }
    // Render if we never have, or if our content was stripped while we were
    // detached (HA cached us through a connection drop, or SPA nav away/back).
    // _render's self-heal rebuilds the shell when .tm-shell is missing.
    if (!this._rendered || !this.querySelector(".tm-shell")) this._render();
  }
  disconnectedCallback() {
    this.removeEventListener("focusin", this._onFocusIn);
    this.removeEventListener("click", this._onClick);
    this.removeEventListener("dblclick", this._onDblClick);
    this.removeEventListener("input", this._onInput);
    this.removeEventListener("change", this._onChange);
    this.removeEventListener("value-changed", this._onValueChanged);
    this.removeEventListener("keydown", this._onKeyDown);
    this.removeEventListener("dragstart", this._onDragStart);
    this.removeEventListener("dragover", this._onDragOver);
    this.removeEventListener("drop", this._onDrop);
    document.removeEventListener("visibilitychange", this._onVisibilityChange);
    this._closeRowMenu();
  }

  _onVisibilityChange() {
    if (document.visibilityState !== "visible") return;
    if (!this._hass) return;
    // Tab woke up. HA may have stripped our DOM while the socket was down —
    // repaint cached state immediately if the shell is gone, then always
    // refetch so returning to the tab shows current data. This mirrors how
    // Lovelace repaints on reconnect and also clears any staleness from
    // changes made on another device while the tab was idle.
    if (!this.querySelector(".tm-shell")) this._render();
    this._fetchState();
  }

  _isHaDark() {
    const el = this.isConnected ? this : document.documentElement;
    const bg = getComputedStyle(el)
      .getPropertyValue("--primary-background-color").trim();
    if (!bg) return false;
    let r, g, b;
    if (bg.startsWith("#")) {
      const h = bg.slice(1);
      r = parseInt(h.substr(0, 2), 16);
      g = parseInt(h.substr(2, 2), 16);
      b = parseInt(h.substr(4, 2), 16);
    } else {
      const m = bg.match(/\d+/g);
      if (m) { r = +m[0]; g = +m[1]; b = +m[2]; }
    }
    if (r === undefined) return false;
    return (r * 299 + g * 587 + b * 114) / 1000 < 128;
  }


  _t(key, params) {
    const fn = window.__taskmate_localize;
    return fn ? fn(this._hass, key, params) : key;
  }

  // Reason prefixes for *derived* (automatic) transactions, which can't be
  // undone in isolation — mirrors the backend deny-list in coord_points.py and
  // the copy in taskmate-activity-card.js. Everything else (penalty, bonus,
  // gift, manual add/remove) is reversible.
  //
  // This is a deny-list, NOT an allow-list (#761): manual adjustments carry
  // arbitrary or empty reasons, so they cannot be recognised positively.
  // tests/test_panel_undo_deny_list.py pins all three copies together.
  static get _UNDO_DENY_PREFIXES() {
    return [
      "Weekend bonus", "Streak milestone bonus", "Perfect week bonus",
      "Allocated to pool:", "Pool refund", "Points decay",
      "Savings interest", "Badge",
    ];
  }

  _txnReversible(reason) {
    const r = reason || "";
    return !TaskMatePanel._UNDO_DENY_PREFIXES.some(p => r.startsWith(p));
  }

  // Transaction reasons are stored in English in the DB (e.g. "Penalty: Messy room").
  // Mirror the activity-card mapping so panel views render in the user's language.
  _translateReason(reason) {
    if (!reason) return reason;
    const prefixMap = [
      ['Allocated to pool:', 'activity.reason_allocated_to_pool'],
      ['Pool refund (reward expired):', 'activity.reason_pool_refund_expired'],
      ['Pool refund (reward sold out):', 'activity.reason_pool_refund_sold_out'],
      ['Pool refund (reward cost reduced):', 'activity.reason_pool_refund_cost_reduced'],
      ['Pool refund (reward deleted):', 'activity.reason_pool_refund_deleted'],
      ['Penalty:', 'activity.reason_penalty'],
      ['Bonus:', 'activity.reason_bonus'],
    ];
    for (const [prefix, key] of prefixMap) {
      if (reason.startsWith(prefix)) {
        const name = reason.slice(prefix.length).trim();
        return this._t(key, { name });
      }
    }
    if (reason.startsWith('Perfect week bonus!')) {
      return this._t('activity.reason_perfect_week');
    }
    // Manual adjustment from the admin panel (#746). An exact match rather than a
    // prefix: unlike "Bonus: <name>" there is no trailing detail to extract.
    if (reason === 'Admin panel adjustment') {
      return this._t('activity.reason_admin_adjustment');
    }
    const weekendMatch = reason.match(/^Weekend bonus \(×(\d+)\)$/);
    if (weekendMatch) {
      return this._t('activity.reason_weekend_bonus', { multiplier: weekendMatch[1] });
    }
    const streakMatch = reason.match(/^Streak milestone bonus \((\d+) day streak!\)$/);
    if (streakMatch) {
      return this._t('activity.reason_streak_milestone', { days: streakMatch[1] });
    }
    return reason;
  }

  // ---- state -----------------------------------------------------------
  async _fetchState(_attempt = 0) {
    if (!this._hass) return;
    this._loading = true;
    this._render();
    try {
      this._state = await this._hass.callWS({ type: "taskmate/get_state" });
      this._error = null;
    } catch (err) {
      // Retry once after a short delay — covers the race where the WS is
      // still mid-reconnect when we make the call.
      if (_attempt < 1) {
        this._loading = false;
        await new Promise(r => setTimeout(r, 800));
        return this._fetchState(_attempt + 1);
      }
      this._error = (err && err.message) || String(err);
      this._state = null;
    }
    // Fetch notifications state separately — failure here must not abort
    // the main state load or its retry logic.
    try {
      this._notifState = await this._hass.callWS({ type: "taskmate/notifications/get_state" });
      this._notifyServices = await this._hass.callWS({ type: "taskmate/notifications/list_notify_services" });
    } catch (e) {
      console.warn("TaskMate notifications state fetch failed", e);
      this._notifState = null;
      this._notifyServices = [];
    }
    this._loading = false;
    this._render();
  }

  async _callWS(payload) {
    try {
      const res = await this._hass.callWS(payload);
      return { ok: true, res };
    } catch (err) {
      return { ok: false, err: (err && err.message) || String(err) };
    }
  }

  _showToast(kind, text) {
    const existing = this.querySelector(".tm-toast");
    if (existing) existing.remove();
    const node = document.createElement("div");
    node.className = `tm-toast tm-toast-${kind}`;
    node.textContent = text;
    this.appendChild(node);
    setTimeout(() => { if (node.isConnected) node.remove(); }, 3500);
  }

  _hashDialog() {
    return this._dialog ? JSON.stringify(this._dialog.data) : "";
  }

  _closeDialog(force = false) {
    if (!force && this._dialog && this._dialogInitialHash != null && this._hashDialog() !== this._dialogInitialHash) {
      if (!confirm(this._t("panel.confirm_unsaved"))) return;
    }
    this._dialog = null;
    this._dialogInitialHash = null;
    this._render();
  }

  _openDialog(d) {
    this._dialog = d;
    this._dialogInitialHash = this._hashDialog();
    this._render();
  }

  // ---- event delegation ------------------------------------------------
  _onClick(e) {
    if (!e.target.closest(".tm-ep")) this._closeEntityDropdown();
    // Row action menu: dismiss on any click that isn't the kebab toggle itself.
    // Clicks on a menu item still resolve their action below (e.target's parent
    // chain stays intact after the menu is detached), then the menu closes.
    if (this._rowMenuEl && !e.target.closest('[data-act="toggle-row-menu"]')) this._closeRowMenu();
    const epSelected = e.target.closest(".tm-ep-selected");
    if (epSelected && !e.target.closest(".tm-entity-clear")) {
      const picker = epSelected.closest(".tm-ep");
      const field = picker?.dataset?.epField;
      if (field) {
        this._setEntityField(field, "");
        this._render();
        requestAnimationFrame(() => {
          const input = this.querySelector(`.tm-ep[data-ep-field="${field}"] .tm-ep-input`);
          if (input) { input.focus(); }
        });
      }
      return;
    }
    const t = e.target.closest("[data-act]");
    if (!t) return;
    const act = t.dataset.act;

    if (act === "tab")          {
      this._activeTab = t.dataset.tab; this._filter = ""; this._mobileNavOpen = false; this._render();
      // Settings tab lists HA users for the parent-role picker (#661); load then repaint.
      if (this._activeTab === "settings") this._ensureHaUsers().then(() => this._render());
      return;
    }
    if (act === "toggle-mobile-nav") { this._mobileNavOpen = !this._mobileNavOpen; this._render(); return; }
    if (act === "close-mobile-nav")  { this._mobileNavOpen = false; this._render(); return; }
    if (act === "close-dialog") { this._closeDialog(); return; }
    if (act === "clear-field") { this._setEntityField(t.dataset.field, ""); this._render(); return; }
    if (act === "pick-entity") {
      // The allowlist picker (#678) isn't a dialog field — picking there adds
      // the entity to the saved list rather than writing into _dialog.data.
      if (t.dataset.field === "_allowlist_pick") {
        this._closeEntityDropdown();
        this._addToAllowlist(t.dataset.value);
        return;
      }
      this._setEntityField(t.dataset.field, t.dataset.value); this._closeEntityDropdown(); this._render(); return;
    }
    if (act === "scrim") {
      if (e.target === this.querySelector(".tm-scrim")) this._closeDialog();
      return;
    }
    if (act === "retry")        { this._fetchState(); return; }
    if (act === "switch-to-activity") { this._activeTab = "activity"; this._render(); return; }
    if (act === "toggle-ha-menu") { this.dispatchEvent(new CustomEvent("hass-toggle-menu", { bubbles: true, composed: true })); return; }
    if (act === "copy-id") { navigator.clipboard.writeText(t.dataset.id).then(() => this._showToast("ok", this._t("panel.toast_copied"))); return; }
    if (act === "view-photo") {
      if (window.__taskmate_lightbox) { e.preventDefault(); window.__taskmate_lightbox(t.dataset.photo, t.dataset.cap, this._t("common.close")); }
      return;
    }

    // Children
    if (act === "add-child")    { this._openChildDialog(null); return; }
    if (act === "gift-points")  { this._openGiftDialog(); return; }
    if (act === "save-gift")    { this._doGiftPoints(); return; }
    if (act === "adjust-points") { this._doAdjustPoints(t.dataset.id, Number(t.dataset.delta)); return; }
    if (act === "adjust-points-custom") { this._openAdjustDialog(t.dataset.id); return; }
    if (act === "chore-image-pick")   { this._pickChoreImage(); return; }
    if (act === "chore-image-remove") {
      // Clears the dialog value only — the file is deleted on save by the
      // coordinator, so cancelling the dialog cannot destroy the picture.
      this._dialog.data.image_url = "";
      this._render();
      return;
    }
    if (act === "save-adjust-add")    { this._saveAdjustDialog(1); return; }
    if (act === "save-adjust-remove") { this._saveAdjustDialog(-1); return; }
    if (act === "edit-child")   { this._openChildDialog(t.dataset.id); return; }
    if (act === "delete-child") { this._confirmDelete("child", t.dataset.id); return; }
    if (act === "save-child")   { this._doSaveChild(); return; }
    if (act === "reorder-chores-for-child") { this._openReorderDialog(t.dataset.id); return; }
    if (act === "reorder-move") { this._moveInReorder(t.dataset.dragId, Number(t.dataset.dir)); return; }
    if (act === "save-chore-order") { this._dialog && this._dialog.data && this._dialog.data.global ? this._doSaveGlobalChoreOrder() : this._doSaveChoreOrder(); return; }
    if (act === "reorder-chores-global") { this._openGlobalReorderDialog(); return; }

    // Chores
    if (act === "add-chore")    { this._openChoreDialog(null); return; }
    if (act === "edit-chore")   { this._openChoreDialog(t.dataset.id); return; }
    if (act === "toggle-row-menu") { this._toggleRowMenu(t); return; }
    if (act === "clone-chore")  { this._doCloneChore(t.dataset.id); return; }
    if (act === "request-swap") { this._openSwapDialog(t.dataset.id); return; }
    if (act === "save-swap")    { this._doRequestSwap(); return; }
    if (act === "approve-swap") { this._doSwap("approve", t.dataset.id); return; }
    if (act === "reject-swap")  { this._doSwap("reject", t.dataset.id); return; }
    if (act === "bulk-toggle")  { this._bulkMode = !this._bulkMode; this._bulkSel.clear(); this._render(); return; }
    if (act === "bulk-enable")  { this._doBulk("enable"); return; }
    if (act === "bulk-disable") { this._doBulk("disable"); return; }
    if (act === "bulk-delete")  { this._doBulk("delete"); return; }
    if (act === "bulk-reassign") {
      const sel = this.querySelector("[data-role='bulk-reassign']");
      const v = sel ? sel.value : "";
      if (!v) { this._showToast("err", this._t("panel.bulk_pick_target")); return; }
      this._doBulk("reassign", v === "__all__" ? [] : [v]); return;
    }
    if (act === "delete-chore") { this._confirmDelete("chore", t.dataset.id); return; }
    if (act === "parent-complete-chore") { this._doParentComplete(t.dataset.id); return; }
    if (act === "skip-chore") { this._doSkipChore(t.dataset.id); return; }
    if (act === "toggle-chore-active") { this._doToggleChoreActive(t.dataset.id); return; }
    if (act === "save-chore")   { this._doSaveChore(); return; }
    if (act === "bulk-add-chore") { this._openBulkAddDialog(); return; }
    if (act === "save-bulk-chores") { this._doSaveBulkChores(); return; }
    if (act === "rename-chore-start") { this._startInlineRename("chore", t.dataset.id); return; }
    if (act === "rename-chore-commit") { this._commitInlineRename(); return; }
    if (act === "rename-chore-cancel") { this._inlineRename = null; this._render(); return; }
    if (act === "toggle-day")        { this._toggleArrayField("due_days", t.dataset.day); return; }
    if (act === "toggle-bulk-day")   { this._toggleArrayField("due_days", t.dataset.day); return; }
    if (act === "toggle-assigned")   { this._toggleArrayField("assigned_to", t.dataset.id); return; }
    if (act === "toggle-depends")    { this._toggleArrayField("depends_on", t.dataset.id); return; }
    if (act === "toggle-calendar")   { this._toggleArrayField("publish_calendar_entities", t.dataset.id); return; }
    if (act === "toggle-weather-condition") { this._toggleArrayField("weather_block_conditions", t.dataset.id); return; }
    if (act === "insights-view") {
      this._insightView = t.dataset.id;
      this._render();
      return;
    }
    if (act === "health-refresh") { this._health = null; this._loadHealth(); return; }
    if (act === "health-goto") {
      // Jump straight to where the problem lives rather than describing it.
      const target = { chores: "chores", rewards: "rewards", children: "children",
                       settings: "settings", activity: "activity" }[t.dataset.id];
      if (target) { this._activeTab = target; this._render(); }
      return;
    }
    if (act === "projection-window") {
      this._projectionDays = Number(t.dataset.id);
      this._projection = null;
      this._loadProjection(this._projectionDays);
      return;
    }
    if (act === "friction-window") {
      this._frictionDays = Number(t.dataset.id);
      this._friction = null;
      this._loadFriction(this._frictionDays);
      return;
    }
    if (act === "insights-window") {
      this._insightDays = Number(t.dataset.id);
      this._fairness = null;
      this._loadFairness(this._insightDays);
      return;
    }
    if (act === "remove-allowlist")         { this._removeFromAllowlist(t.dataset.id); return; }
    if (act === "add-scheduled")            { this._addScheduledChange(); return; }
    if (act === "remove-scheduled")         { this._removeScheduledChange(t.dataset.id); return; }
    if (act === "toggle-scheduled-child" || act === "toggle-scheduled-day") {
      this._toggleScheduledListValue(t.dataset.id); return;
    }
    if (act === "add-bonus-subtask") { this._addBonusSubtask(); return; }
    if (act === "remove-bonus-subtask") { this._removeBonusSubtask(Number(t.dataset.idx)); return; }

    // Rewards
    if (act === "add-reward")    { this._openRewardDialog(null); return; }
    if (act === "edit-reward")   { this._openRewardDialog(t.dataset.id); return; }
    if (act === "delete-reward") { this._confirmDelete("reward", t.dataset.id); return; }
    if (act === "save-reward")   { this._doSaveReward(); return; }
    if (act === "toggle-reward-assigned") { this._toggleArrayField("assigned_to", t.dataset.id); return; }

    // Quests (chore chains)
    if (act === "add-quest")    { this._openQuestDialog(null); return; }
    if (act === "edit-quest")   { this._openQuestDialog(t.dataset.id); return; }
    if (act === "delete-quest") { this._confirmDelete("quest", t.dataset.id); return; }
    if (act === "save-quest")   { this._doSaveQuest(); return; }
    if (act === "toggle-quest-assigned") { this._toggleArrayField("assigned_to", t.dataset.id); return; }
    if (act === "toggle-quest-step") { this._toggleArrayField("steps", t.dataset.id); return; }
    if (act === "quest-step-move") { this._moveQuestStep(Number(t.dataset.idx), t.dataset.dir); return; }

    // Challenges (daily / weekly)
    if (act === "add-challenge")    { this._openChallengeDialog(null); return; }
    if (act === "edit-challenge")   { this._openChallengeDialog(t.dataset.id); return; }
    if (act === "delete-challenge") { this._confirmDelete("challenge", t.dataset.id); return; }
    if (act === "save-challenge")   { this._doSaveChallenge(); return; }
    if (act === "toggle-challenge-assigned") { this._toggleArrayField("assigned_to", t.dataset.id); return; }

    // Avatar unlockables
    if (act === "manage-avatars")     { this._openAvatarCatalogDialog(); return; }
    if (act === "avatar-add-row")     { this._avatarAddRow(); return; }
    if (act === "avatar-del-row")     { this._avatarDelRow(Number(t.dataset.idx)); return; }
    if (act === "save-avatar-catalog") { this._doSaveAvatarCatalog(); return; }

    // Penalties
    if (act === "add-penalty")    { this._openPenBonDialog("penalty", null); return; }
    if (act === "edit-penalty")   { this._openPenBonDialog("penalty", t.dataset.id); return; }
    if (act === "delete-penalty") { this._confirmDelete("penalty", t.dataset.id); return; }
    if (act === "save-penalty")   { this._doSavePenBon("penalty"); return; }
    if (act === "apply-penalty")  { this._openApplyDialog("penalty", t.dataset.id); return; }
    if (act === "do-apply-penalty") { this._doApplyPenBon("penalty", t.dataset.id, t.dataset.child); return; }

    // Bonuses
    if (act === "add-bonus")    { this._openPenBonDialog("bonus", null); return; }
    if (act === "edit-bonus")   { this._openPenBonDialog("bonus", t.dataset.id); return; }
    if (act === "delete-bonus") { this._confirmDelete("bonus", t.dataset.id); return; }
    if (act === "save-bonus")   { this._doSavePenBon("bonus"); return; }
    if (act === "apply-bonus")  { this._openApplyDialog("bonus", t.dataset.id); return; }
    if (act === "do-apply-bonus") { this._doApplyPenBon("bonus", t.dataset.id, t.dataset.child); return; }

    if (act === "toggle-penbon-assigned") { this._toggleArrayField("assigned_to", t.dataset.id); return; }

    // Templates
    if (act === "tpl-add-from")     { this._activeTab = "templates"; this._templateView = "picker"; this._render(); return; }
    if (act === "tpl-select")       { this._selectTemplate(t.dataset.id); return; }
    if (act === "tpl-back")         { this._templateView = null; this._templateSelected = null; this._templateChores = []; this._render(); return; }
    if (act === "tpl-remove-chore") { this._templateChores.splice(Number(t.dataset.idx), 1); this._render(); return; }
    if (act === "tpl-toggle-expand"){ const idx = Number(t.dataset.idx); if (this._templateChores[idx]) { this._templateChores[idx]._expanded = !this._templateChores[idx]._expanded; this._render(); } return; }
    if (act === "tpl-apply")        { this._doApplyTemplate(); return; }
    if (act === "tpl-save-from")    { this._saveTemplateDialog = true; this._render(); return; }
    if (act === "tpl-save-confirm") { this._doSaveFromChores(); return; }
    if (act === "tpl-save-cancel")  { this._saveTemplateDialog = false; this._render(); return; }
    if (act === "tpl-create")       { this._openCreateTemplateDialog(); return; }
    if (act === "tpl-edit")         { this._openEditTemplateDialog(t.dataset.id); return; }
    if (act === "tpl-delete")       { this._confirmDeleteTemplate(t.dataset.id); return; }
    if (act === "tpl-save-created") { this._doSaveCreatedTemplate(); return; }
    if (act === "tpl-save-edited")  { this._doSaveEditedTemplate(); return; }
    if (act === "tpl-toggle-day")   { const idx = Number(t.dataset.idx); const day = t.dataset.day; if (this._templateChores[idx]) { const days = this._templateChores[idx].due_days || []; const pos = days.indexOf(day); if (pos >= 0) days.splice(pos, 1); else days.push(day); this._templateChores[idx].due_days = days; this._render(); } return; }
    if (act === "tpl-dialog-add-chore") { if (this._dialog?.data?.chores) { this._dialog.data.chores.push({ name: "", points: 5, time_category: "anytime", schedule_mode: "specific_days", due_days: [], requires_approval: false, assignment_mode: "everyone", daily_limit: 1, completion_sound: "coin" }); this._render(); } return; }
    if (act === "tpl-dialog-remove-chore") { if (this._dialog?.data?.chores) { this._dialog.data.chores.splice(Number(t.dataset.idx), 1); this._render(); } return; }

    // Groups
    if (act === "add-group")    { this._openGroupDialog(null); return; }
    if (act === "edit-group")   { this._openGroupDialog(t.dataset.id); return; }
    if (act === "delete-group") { this._confirmDelete("group", t.dataset.id); return; }
    if (act === "save-group")   { this._doSaveGroup(); return; }
    if (act === "toggle-group-chore") { this._toggleArrayField("chore_ids", t.dataset.id); return; }

    // Badges
    if (act === "badges-subtab") { this._badgesSubTab = t.dataset.sub; this._render(); return; }
    if (act === "add-badge")    { this._openBadgeDialog(null); return; }
    if (act === "edit-badge")   { this._openBadgeDialog(t.dataset.id); return; }
    if (act === "delete-badge") { this._confirmDeleteBadge(t.dataset.id); return; }
    if (act === "save-badge")   { this._doSaveBadge(); return; }
    if (act === "toggle-badge-enabled") { this._doToggleBadgeEnabled(t.dataset.id, t.dataset.enabled === "true"); return; }
    if (act === "award-badge")  { this._openAwardDialog(t.dataset.id); return; }
    if (act === "do-award-badge") { this._doAwardBadge(t.dataset.id, this._dialog?.data?.child_id || ""); return; }
    if (act === "revoke-badge") { this._doRevokeBadge(t.dataset.id, t.dataset.name); return; }
    if (act === "rebuild-badges") { this._doRebuildBadges(); return; }
    if (act === "badge-add-criterion") { this._syncIconPickers(); if (this._dialog?.data?.criteria) { this._dialog.data.criteria.push({ metric: "total_points", operator: ">=", value: 1 }); this._render(); } return; }
    if (act === "badge-remove-criterion") { this._syncIconPickers(); if (this._dialog?.data?.criteria) { this._dialog.data.criteria.splice(Number(t.dataset.idx), 1); this._render(); } return; }
    if (act === "badge-toggle-assigned") { this._syncIconPickers(); this._toggleArrayField("assigned_to", t.dataset.id); return; }

    // Notifications tab
    if (act === "notif-set-master")       { this._notifSetMaster(t.dataset.typeId, t.checked); return; }
    if (act === "notif-send-test")        { this._doSendTestNotif(t.dataset.typeId); return; }
    if (act === "notif-set-route")        { this._notifSetRoute(t.dataset.typeId, t.dataset.recipientId, t.checked, t.dataset.time || null); return; }
    if (act === "notif-set-route-time")   { /* handled in _onChange */ return; }
    if (act === "notif-set-child-notify") { /* handled in _onChange */ return; }
    if (act === "notif-set-parent-notify"){ /* handled in _onChange */ return; }
    if (act === "notif-rename-parent")    { /* handled in _onChange */ return; }
    if (act === "notif-set-streak-cutoff"){ /* handled in _onChange */ return; }
    if (act === "notif-set-nav-url-global"){ /* handled in _onChange */ return; }
    if (act === "notif-set-nav-url-type")  { /* handled in _onChange */ return; }
    if (act === "notif-set-group-global") { /* handled in _onChange */ return; }
    if (act === "notif-set-group-type")   { /* handled in _onChange */ return; }
    if (act === "notif-add-parent")       { this._notifAddParent(); return; }
    if (act === "notif-delete-parent")    { this._notifDeleteParent(t.dataset.parentId); return; }
    if (act === "notif-add-custom")       { this._notifAddCustom(); return; }
    if (act === "notif-delete-custom")    { this._notifDeleteCustom(t.dataset.customId); return; }
    if (act === "notif-update-custom")    { /* handled in _onChange */ return; }
    if (act === "notif-toggle-custom")    { this._notifToggleCustom(t.dataset.customId, t.checked); return; }
    if (act === "notif-toggle-day")       { this._notifToggleDay(t.dataset.customId, Number(t.dataset.day), t.checked); return; }
    if (act === "notif-toggle-recipient") { this._notifToggleRecipient(t.dataset.customId, t.dataset.recipientId); return; }

    // Settings
    if (act === "save-settings") { this._doSaveSettings(); return; }
    if (act === "config-export") { this._doExportConfig(); return; }
    if (act === "config-import") { this.querySelector("[data-role='config-import-file']")?.click(); return; }
    if (act === "ics-show") { this._icsShow(); return; }
    if (act === "ics-regenerate") { this._icsRegenerate(); return; }
    if (act === "ics-copy") { this._icsCopy(); return; }

    // Audit log
    if (act === "audit-clear") { this._doClearAudit(); return; }

    // Undo a points transaction (penalty / bonus)
    if (act === "undo-tx") { this._doUndoTransaction(t.dataset.id); return; }

    // Time-of-day period editor
    if (act === "tp-add") {
      this._syncTimePeriodInputs();
      this._timePeriodsDraft.push({ id: "", label: "", start: "", end: "", icon: "mdi:clock-outline" });
      this._render();
      return;
    }
    if (act === "tp-remove") {
      this._syncTimePeriodInputs();
      this._timePeriodsDraft.splice(Number(t.dataset.idx), 1);
      this._render();
      return;
    }
    if (act === "tp-reset") {
      this._timePeriodsDraft = this._defaultTimePeriods();
      this._render();
      return;
    }

    // Vacation / pause-mode editor
    if (act === "vac-add") {
      this._syncVacationInputs();
      this._vacationDraft.push({ id: "", name: "", start: "", end: "" });
      this._render();
      return;
    }
    if (act === "vac-remove") {
      this._syncVacationInputs();
      this._vacationDraft.splice(Number(t.dataset.idx), 1);
      this._render();
      return;
    }

    // Activity / approvals
    if (act === "approve-all-chores") { this._doApproveAll(); return; }
    if (act === "approve-chore")  { this._doApprove("chore", t.dataset.id); return; }
    if (act === "reject-chore")   { this._doReject("chore", t.dataset.id); return; }
    if (act === "approve-reward") { this._doApprove("reward", t.dataset.id); return; }
    if (act === "reject-reward")  { this._doReject("reward", t.dataset.id); return; }

    // Filter clear
    if (act === "clear-filter") { this._filter = ""; this._render(); return; }
  }

  _onDblClick(e) {
    const t = e.target.closest("[data-rename]");
    if (!t) return;
    this._startInlineRename(t.dataset.rename, t.dataset.id);
  }

  _onFocusIn(e) {
    if (e.target.classList?.contains("tm-ep-input")) {
      e.target.value = "";
      this._openEntityDropdown(e.target);
    }
  }

  _onInput(e) {
    const t = e.target;
    if (!t.dataset) return;
    if (t.classList?.contains("tm-ep-input")) {
      this._openEntityDropdown(t);
      return;
    }
    // Filter input
    if (t.dataset.filter === "true") {
      this._filter = t.value || "";
      // Re-render only the visible tab body — but full re-render is fine for now.
      this._render();
      // Restore focus to the filter input
      const f = this.querySelector("[data-filter='true']");
      if (f) { f.focus(); f.setSelectionRange(this._filter.length, this._filter.length); }
      return;
    }
    // Inline rename input
    if (t.dataset.field === "_inlineRename") {
      this._inlineRename.value = t.value;
      return;
    }
    // Time-of-day period editor field edits
    const tpField = t.dataset?.tpField;
    const tpIdx = t.dataset?.tpIdx;
    if (tpField && tpIdx != null && this._timePeriodsDraft?.[Number(tpIdx)]) {
      this._timePeriodsDraft[Number(tpIdx)][tpField] = t.value;
      return;
    }
    // Vacation editor field edits
    const vacField = t.dataset?.vacField;
    const vacIdx = t.dataset?.vacIdx;
    if (vacField && vacIdx != null && this._vacationDraft?.[Number(vacIdx)]) {
      this._vacationDraft[Number(vacIdx)][vacField] = t.value;
      return;
    }
    // Template preview chore field edits
    const tplField = t.dataset?.tplField;
    const tplIdx = t.dataset?.tplIdx;
    if (tplField && tplIdx != null && this._templateChores[Number(tplIdx)]) {
      let value = t.value;
      if (tplField === "points" || tplField === "daily_limit") value = Math.max(0, parseInt(value) || 0);
      if (tplField === "requires_approval") value = value === "true";
      this._templateChores[Number(tplIdx)][tplField] = value;
      return;
    }
    // Template dialog chore field edits
    const dField = t.dataset?.tplDialogField;
    const dIdx = t.dataset?.tplDialogIdx;
    if (dField && dIdx != null && this._dialog?.data?.chores?.[Number(dIdx)]) {
      let value = t.value;
      if (dField === "points") value = Math.max(0, parseInt(value) || 0);
      this._dialog.data.chores[Number(dIdx)][dField] = value;
      return;
    }
    // Badge criterion field edits
    const bcField = t.dataset?.badgeCriterionField;
    const bcIdx = t.dataset?.badgeCriterionIdx;
    if (bcField && bcIdx != null && this._dialog?.data?.criteria?.[Number(bcIdx)]) {
      const val = (t.type === "number") ? (t.value === "" ? 1 : Number(t.value)) : t.value;
      this._dialog.data.criteria[Number(bcIdx)][bcField] = val;
      if (bcField === "metric") this._render(); // re-render to show/hide value input for bool metrics
      return;
    }
    // Scheduled-change draft (#675) lives beside the dialog data, not in it —
    // it is a separate record, not a property of the chore being edited.
    if (this._dialog && t.dataset.field && t.dataset.field.startsWith("_sched_")) {
      const key = t.dataset.field.slice("_sched_".length);
      const draft = this._schedDraft();
      draft[key] = t.value;
      // Switching field changes which value control is shown, and any list
      // selection from the previous field no longer applies.
      if (key === "field") { draft.value = ""; draft.value_list = []; this._render(); }
      return;
    }

    // Dialog field
    if (this._dialog && t.dataset.field) {
      const field = t.dataset.field;
      // ha-switch / checkbox elements fire `input` on toggle in newer HA; their
      // `value` is the constant string "on", so read `.checked` instead — otherwise
      // a switch can never be set false (it always stores a truthy string). Mirrors _onChange.
      let value;
      if (t.type === "checkbox" || t.tagName === "HA-SWITCH") value = t.checked;
      else value = (t.type === "number") ? (t.value === "" ? null : Number(t.value)) : t.value;
      const arrMatch = field.match(/^(\w+)\[(\d+)\]\.(\w+)$/);
      if (arrMatch) {
        const [, arr, idx, prop] = arrMatch;
        if (this._dialog.data[arr] && this._dialog.data[arr][Number(idx)] !== undefined) {
          this._dialog.data[arr][Number(idx)][prop] = value;
        }
      } else {
        this._dialog.data[field] = value;
      }
      return;
    }
  }

  _onChange(e) {
    const t = e.target;
    if (!t.dataset) return;
    if (t.dataset.act === "bulk-toggle-row") {
      const id = t.dataset.id;
      if (t.checked) this._bulkSel.add(id); else this._bulkSel.delete(id);
      this._render();
      return;
    }
    if (t.dataset.role === "config-import-file") {
      const file = t.files && t.files[0];
      t.value = "";  // allow re-selecting the same file later
      if (file) this._doImportConfig(file);
      return;
    }
    if (t.dataset.local === "show-ids") {
      this._showIds = t.checked;
      localStorage.setItem("taskmate-show-ids", this._showIds);
      this._render();
      return;
    }
    // Time-of-day period editor (time inputs fire change on blur)
    if (t.dataset?.tpField && t.dataset.tpIdx != null && this._timePeriodsDraft?.[Number(t.dataset.tpIdx)]) {
      this._timePeriodsDraft[Number(t.dataset.tpIdx)][t.dataset.tpField] = t.value;
      return;
    }
    // Vacation editor (date inputs fire change on blur)
    if (t.dataset?.vacField && t.dataset.vacIdx != null && this._vacationDraft?.[Number(t.dataset.vacIdx)]) {
      this._vacationDraft[Number(t.dataset.vacIdx)][t.dataset.vacField] = t.value;
      return;
    }
    // Notifications tab change-driven actions
    if (t.dataset.act === "notif-set-route-time") {
      this._notifSetRoute(t.dataset.typeId, t.dataset.recipientId, true, t.value || null);
      return;
    }
    if (t.dataset.act === "notif-set-child-notify") {
      this._notifSetChildNotify(t.dataset.childId, t.value);
      return;
    }
    if (t.dataset.act === "notif-set-child-quiet") {
      const row = t.closest("[data-quiet-row]");
      const startEl = row && row.querySelector('[data-quiet-bound="start"]');
      const endEl = row && row.querySelector('[data-quiet-bound="end"]');
      this._notifSetChildQuiet(t.dataset.childId, (startEl && startEl.value) || "", (endEl && endEl.value) || "");
      return;
    }
    if (t.dataset.act === "notif-set-parent-notify") {
      const p = (this._notifState && this._notifState.recipients && this._notifState.recipients.parents || []).find(x => x.id === t.dataset.parentId);
      if (p) this._notifUpsertParent({ ...p, notify_service: t.value });
      return;
    }
    if (t.dataset.act === "notif-rename-parent") {
      const p = (this._notifState && this._notifState.recipients && this._notifState.recipients.parents || []).find(x => x.id === t.dataset.parentId);
      if (p) this._notifUpsertParent({ ...p, name: t.value });
      return;
    }
    if (t.dataset.act === "notif-set-streak-cutoff") {
      this._notifSetStreakCutoff(t.value);
      return;
    }
    if (t.dataset.act === "notif-set-nav-url-global") {
      this._notifSetNavUrl(null, t.value);
      return;
    }
    if (t.dataset.act === "notif-set-nav-url-type") {
      this._notifSetNavUrl(t.dataset.typeId, t.value);
      return;
    }
    if (t.dataset.act === "notif-set-group-global") {
      this._notifSetGroup(null, t.value);
      return;
    }
    if (t.dataset.act === "notif-set-group-type") {
      this._notifSetGroup(t.dataset.typeId, t.value);
      return;
    }
    if (t.dataset.act === "notif-set-escalation") {
      this._notifSetEscalation(t.dataset.escField, t.value);
      return;
    }
    if (t.dataset.act === "notif-update-custom") {
      this._notifUpdateCustomField(t.dataset.customId, t.dataset.field, t.value);
      return;
    }
    // Badge criterion select changes
    const bcField2 = t.dataset?.badgeCriterionField;
    const bcIdx2 = t.dataset?.badgeCriterionIdx;
    if (bcField2 && bcIdx2 != null && this._dialog?.data?.criteria?.[Number(bcIdx2)]) {
      this._dialog.data.criteria[Number(bcIdx2)][bcField2] = t.value;
      if (bcField2 === "metric") this._render();
      return;
    }
    // Template preview chore field changes (selects)
    const tplField = t.dataset?.tplField;
    const tplIdx = t.dataset?.tplIdx;
    if (tplField && tplIdx != null && this._templateChores[Number(tplIdx)]) {
      let value = t.value;
      if (tplField === "requires_approval") value = value === "true";
      this._templateChores[Number(tplIdx)][tplField] = value;
      return;
    }
    // Template dialog chore field changes (selects)
    const dField = t.dataset?.tplDialogField;
    const dIdx = t.dataset?.tplDialogIdx;
    if (dField && dIdx != null && this._dialog?.data?.chores?.[Number(dIdx)]) {
      this._dialog.data.chores[Number(dIdx)][dField] = t.value;
      return;
    }
    if (!this._dialog) return;
    if (!t.dataset.field) return;
    let value;
    if (t.type === "checkbox" || t.tagName === "HA-SWITCH") value = t.checked;
    else if (t.type === "number") value = (t.value === "" ? null : Number(t.value));
    else value = t.value;
    this._dialog.data[t.dataset.field] = value;
    if (t.dataset.rerender === "true") this._render();
  }

  _onValueChanged(e) {
    const t = e.target;
    // Period editor icon pickers live outside any dialog
    if (t.dataset?.tpField && t.dataset.tpIdx != null && this._timePeriodsDraft?.[Number(t.dataset.tpIdx)]) {
      const val = e.detail && "value" in e.detail ? e.detail.value : t.value;
      this._timePeriodsDraft[Number(t.dataset.tpIdx)][t.dataset.tpField] = val == null ? "" : val;
      return;
    }
    if (!this._dialog) return;
    if (!t.dataset || !t.dataset.field) return;
    const v = e.detail && "value" in e.detail ? e.detail.value : t.value;
    this._dialog.data[t.dataset.field] = v == null ? "" : v;
  }

  _syncIconPickers() {
    if (!this._dialog) return;
    this.querySelectorAll("ha-icon-picker[data-field]").forEach(el => {
      if (el.value != null) this._dialog.data[el.dataset.field] = el.value;
    });
  }

  _onKeyDown(e) {
    if (e.key === "Escape") {
      if (this._rowMenuEl) { this._closeRowMenu(); return; }
      if (this._inlineRename) { this._inlineRename = null; this._render(); return; }
      if (this._dialog) { this._closeDialog(); return; }
    }
    if (e.key === "Enter" && this._inlineRename) {
      e.preventDefault();
      this._commitInlineRename();
    }
  }

  // ---- Drag and drop (reorder dialog) ----------------------------------
  _onDragStart(e) {
    const t = e.target.closest("[data-drag-id]");
    if (!t) return;
    this._reorderDrag = { id: t.dataset.dragId };
    t.classList.add("tm-dragging");
    e.dataTransfer.effectAllowed = "move";
    e.dataTransfer.setData("text/plain", t.dataset.dragId);
  }

  _onDragOver(e) {
    const t = e.target.closest("[data-drag-id]");
    if (!t || !this._reorderDrag) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
  }

  _onDrop(e) {
    const t = e.target.closest("[data-drag-id]");
    if (!t || !this._reorderDrag || !this._dialog || this._dialog.kind !== "reorder") return;
    e.preventDefault();
    const fromId = this._reorderDrag.id;
    const toId   = t.dataset.dragId;
    if (fromId === toId) { this._reorderDrag = null; this._render(); return; }
    const order = [...(this._dialog.data.order || [])];
    const fromIdx = order.indexOf(fromId);
    const toIdx   = order.indexOf(toId);
    if (fromIdx < 0 || toIdx < 0) return;
    order.splice(fromIdx, 1);
    order.splice(toIdx, 0, fromId);
    this._dialog.data.order = order;
    this._reorderDrag = null;
    this._render();
  }

  // Up/down buttons — the reliable cross-device path (native HTML5 drag does
  // not fire on touchscreens, so mobile users reorder via these).
  _moveInReorder(id, dir) {
    if (!this._dialog || this._dialog.kind !== "reorder") return;
    const order = [...(this._dialog.data.order || [])];
    const from = order.indexOf(id);
    if (from < 0) return;
    const to = from + dir;
    if (to < 0 || to >= order.length) return;
    order.splice(from, 1);
    order.splice(to, 0, id);
    this._dialog.data.order = order;
    this._render();
  }

  _toggleArrayField(field, value) {
    if (!this._dialog || !this._dialog.data) return;
    const arr = Array.isArray(this._dialog.data[field]) ? [...this._dialog.data[field]] : [];
    const i = arr.indexOf(value);
    if (i >= 0) arr.splice(i, 1); else arr.push(value);
    this._dialog.data[field] = arr;
    const body = this.querySelector(".tm-dialog-body");
    const scrollY = body ? body.scrollTop : 0;
    this._render();
    const b = this.querySelector(".tm-dialog-body");
    if (b) b.scrollTop = scrollY;
  }

  _confirmDelete(kind, id) {
    const labels = { child: this._t("panel.entity_child"), chore: this._t("panel.entity_chore"), reward: this._t("panel.entity_reward"), penalty: this._t("panel.entity_penalty"), bonus: this._t("panel.entity_bonus"), group: this._t("panel.entity_group"), quest: this._t("panel.entity_quest"), challenge: this._t("panel.entity_challenge") };
    const collectionKey = { child: "children", chore: "chores", reward: "rewards", penalty: "penalties", bonus: "bonuses", group: "task_groups", quest: "quests", challenge: "challenges" }[kind];
    const item = (this._state[collectionKey] || []).find(x => x.id === id);
    if (!item) return;
    const extraWarn = kind === "child"
      ? `\n\n${this._t("panel.confirm_delete_child_extra")}`
      : kind === "reward"
      ? `\n\n${this._t("panel.confirm_delete_reward_extra")}`
      : kind === "chore"
      ? `\n\n${this._t("panel.confirm_delete_chore_extra")}`
      : "";
    if (!confirm(`${this._t("panel.confirm_delete_prompt", {kind: labels[kind].toLowerCase(), name: item.name})}${extraWarn}\n\n${this._t("panel.confirm_delete_suffix")}`)) return;
    this._doRemove(kind, id);
  }

  async _doRemove(kind, id) {
    const wsType = {
      child:   "taskmate/remove_child",
      chore:   "taskmate/remove_chore",
      reward:  "taskmate/remove_reward",
      penalty: "taskmate/remove_penalty",
      bonus:   "taskmate/remove_bonus",
      group:   "taskmate/remove_task_group",
      quest:   "taskmate/delete_quest",
      challenge: "taskmate/delete_challenge",
    }[kind];
    const idField = {
      child: "child_id", chore: "chore_id", reward: "reward_id",
      penalty: "penalty_id", bonus: "bonus_id", group: "group_id",
      quest: "quest_id", challenge: "challenge_id",
    }[kind];
    const { ok, err } = await this._callWS({ type: wsType, [idField]: id });
    if (!ok) { this._showToast("err", this._t("panel.toast_delete_failed", {error: err})); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_deleted"));
  }

  // ---- Parent complete --------------------------------------------------
  async _doParentComplete(choreId) {
    if (!confirm(this._t("panel.parent_complete_confirm"))) return;
    const { ok, err } = await this._callWS({ type: "taskmate/parent_complete_chore", chore_id: choreId });
    if (!ok) { this._showToast("err", this._t("panel.parent_complete_failed", {error: err})); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.parent_complete_success"));
  }

  // ---- Skip chore --------------------------------------------------------
  async _doSkipChore(choreId) {
    const s = this._state.settings || {};
    if (s.skip_confirmation_enabled !== false && !confirm(this._t("panel.skip_chore_confirm"))) return;
    const { ok, err } = await this._callService("skip_chore", { chore_id: choreId });
    if (!ok) { this._showToast("err", this._t("panel.toast_skip_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_skip_done"));
  }

  async _doToggleChoreActive(choreId) {
    const chore = (this._state.chores || []).find(c => c.id === choreId);
    if (!chore) return;
    const newEnabled = chore.enabled === false;
    const { ok, err } = await this._callWS({ type: "taskmate/update_chore", chore_id: choreId, enabled: newEnabled });
    if (!ok) { this._showToast("err", this._t("panel.toast_toggle_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", newEnabled ? this._t("panel.toast_chore_activated") : this._t("panel.toast_chore_deactivated"));
  }

  // ---- Approvals -------------------------------------------------------
  async _doApprove(kind, id) {
    const wsType = kind === "chore" ? "taskmate/approve_chore" : "taskmate/approve_reward";
    const idField = kind === "chore" ? "completion_id" : "claim_id";
    const { ok, err } = await this._callWS({ type: wsType, [idField]: id });
    if (!ok) { this._showToast("err", this._t("panel.toast_approve_failed", {error: err})); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_approved"));
  }

  async _doApproveAll() {
    const pending = this._state?.pending_completions || [];
    if (pending.length === 0) return;
    if (this._approvingAll) return;
    if (!confirm(this._t("panel.activity_approve_all_confirm", { count: pending.length }))) return;
    // Acknowledge the tap straight away (#799). The batch is fast since #794,
    // but not instant, and a button that looks untouched is indistinguishable
    // from one that did nothing — which is how #794 came to be reported.
    this._approvingAll = true;
    this._render();
    let result;
    try {
      result = await this._callWS({
        type: "taskmate/approve_all_chores",
        completion_ids: pending.map(c => c.id),
      });
    } finally {
      // Cleared the moment the call settles, whatever it did — a button stuck
      // spinning is worse than no busy state at all.
      this._approvingAll = false;
    }
    if (!result.ok) {
      this._showToast("err", this._t("panel.toast_approve_failed", { error: result.err }));
      this._render();
      return;
    }
    await this._fetchState();
    this._showToast("ok", this._t("panel.activity_approve_all_done", { count: pending.length }));
  }

  async _doReject(kind, id) {
    const wsType = kind === "chore" ? "taskmate/reject_chore" : "taskmate/reject_reward";
    const idField = kind === "chore" ? "completion_id" : "claim_id";
    const { ok, err } = await this._callWS({ type: wsType, [idField]: id });
    if (!ok) { this._showToast("err", this._t("panel.toast_reject_failed", {error: err})); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_rejected"));
  }

  // ---- Children --------------------------------------------------------
  async _openChildDialog(id) {
    await this._ensureHaUsers();
    if (id) {
      const c = (this._state.children || []).find(x => x.id === id);
      if (!c) return;
      this._openDialog({ kind: "child", mode: "edit", data: {
        id: c.id, name: c.name || "", avatar: c.avatar || "mdi:account-circle",
        availability_entity: c.availability_entity || "",
        availability_inverted: !!c.availability_inverted,
        unavailability_entity: c.unavailability_entity || "",
        pause_streak_when_unavailable: !!c.pause_streak_when_unavailable,
        linked_user_id: c.linked_user_id || "",
      } });
    } else {
      this._openDialog({ kind: "child", mode: "add", data: {
        name: "", avatar: "mdi:account-circle", availability_entity: "",
        availability_inverted: false, unavailability_entity: "",
        pause_streak_when_unavailable: false,
        linked_user_id: "",
      } });
    }
  }

  async _ensureHaUsers() {
    if (this._haUsers) return;
    const { ok, res } = await this._callWS({ type: "taskmate/list_ha_users" });
    this._haUsers = (ok && res && Array.isArray(res.users)) ? res.users : [];
  }

  async _doSaveChild() {
    this._syncIconPickers();
    const d = this._dialog.data;
    if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_name_required")); return; }
    const wasAdd = this._dialog.mode === "add";
    const payload = wasAdd
      ? { type: "taskmate/add_child", name: d.name.trim(), avatar: d.avatar || "mdi:account-circle",
          availability_entity: d.availability_entity || "", availability_inverted: !!d.availability_inverted,
          unavailability_entity: d.unavailability_entity || "",
          pause_streak_when_unavailable: !!d.pause_streak_when_unavailable, linked_user_id: d.linked_user_id || "" }
      : { type: "taskmate/update_child", child_id: d.id, name: d.name.trim(), avatar: d.avatar || "mdi:account-circle",
          availability_entity: d.availability_entity || "", availability_inverted: !!d.availability_inverted,
          unavailability_entity: d.unavailability_entity || "",
          pause_streak_when_unavailable: !!d.pause_streak_when_unavailable, linked_user_id: d.linked_user_id || "" };
    const { ok, err } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", wasAdd ? this._t("panel.toast_child_added") : this._t("panel.toast_child_updated"));
  }

  // ---- Chore reorder ---------------------------------------------------
  _openReorderDialog(child_id) {
    const child = (this._state.children || []).find(c => c.id === child_id);
    if (!child) return;
    const childChores = (this._state.chores || []).filter(c =>
      c.assignment_mode !== "unassigned" && ((c.assigned_to || []).length === 0 || (c.assigned_to || []).includes(child_id))
    );
    // Use the child's stored chore_order if present, else fall back to the chore list order.
    const existingOrder = (child.chore_order || []).filter(id => childChores.find(c => c.id === id));
    const missing = childChores.map(c => c.id).filter(id => !existingOrder.includes(id));
    const order = [...existingOrder, ...missing];
    this._openDialog({ kind: "reorder", mode: "edit", data: { child_id, order, name: child.name } });
  }

  async _doSaveChoreOrder() {
    const d = this._dialog.data;
    const { ok, err } = await this._callWS({
      type: "taskmate/set_chore_order",
      child_id: d.child_id,
      chore_order: d.order || [],
    });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_order_saved"));
  }

  // ---- Global chore reorder ---------------------------------------------
  _openGlobalReorderDialog() {
    const allChores = this._state.chores || [];
    if (allChores.length < 2) return;
    const existingOrder = (this._state.chore_display_order || []).filter(id => allChores.find(c => c.id === id));
    const missing = allChores.map(c => c.id).filter(id => !existingOrder.includes(id));
    const order = [...existingOrder, ...missing];
    this._openDialog({ kind: "reorder", mode: "edit", data: { global: true, order, name: this._t("panel.tab_chores") } });
  }

  async _doSaveGlobalChoreOrder() {
    const d = this._dialog.data;
    const { ok, err } = await this._callWS({
      type: "taskmate/set_global_chore_order",
      chore_order: d.order || [],
    });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_order_saved"));
  }

  // ---- Bulk add chores -------------------------------------------------
  _openBulkAddDialog() {
    this._openDialog({ kind: "bulk-chore", mode: "add", data: {
      chore_names: "",
      points: 10,
      assigned_to: [],
      requires_approval: true,
      time_category: "anytime",
      schedule_mode: "specific_days",
      due_days: [],
      daily_limit: 1,
      completion_sound: "coin",
    } });
  }

  async _doSaveBulkChores() {
    const d = this._dialog.data;
    const names = (d.chore_names || "").split(/[\n,]/).map(s => s.trim()).filter(Boolean);
    if (names.length === 0) { this._showToast("err", this._t("panel.toast_enter_chore_name")); return; }
    const { ok, err, res } = await this._callWS({
      type: "taskmate/add_chores_bulk",
      chore_names: names,
      points: Number(d.points) || 10,
      assigned_to: d.assigned_to || [],
      depends_on: d.depends_on || [],
      requires_approval: !!d.requires_approval,
      time_category: d.time_category || "anytime",
      schedule_mode: d.schedule_mode || "specific_days",
      due_days: d.due_days || [],
      daily_limit: Number(d.daily_limit) || 1,
      completion_sound: d.completion_sound || "coin",
      icon: d.icon || "",
    });
    if (!ok) { this._showToast("err", this._t("panel.toast_bulk_add_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_bulk_added", {count: (res && res.count) || names.length}));
  }

  // ---- Inline rename ---------------------------------------------------
  _startInlineRename(kind, id) {
    if (kind !== "chore") return;
    const c = (this._state.chores || []).find(x => x.id === id);
    if (!c) return;
    this._inlineRename = { kind: "chore", id, value: c.name };
    this._render();
    const inp = this.querySelector("[data-field='_inlineRename']");
    if (inp) { inp.focus(); inp.select(); }
  }

  async _commitInlineRename() {
    if (!this._inlineRename) return;
    const { kind, id, value } = this._inlineRename;
    const newName = (value || "").trim();
    if (!newName) { this._inlineRename = null; this._render(); return; }
    if (kind === "chore") {
      const c = (this._state.chores || []).find(x => x.id === id);
      if (c && c.name === newName) { this._inlineRename = null; this._render(); return; }
      const { ok, err } = await this._callWS({ type: "taskmate/update_chore", chore_id: id, name: newName });
      this._inlineRename = null;
      if (!ok) { this._showToast("err", this._t("panel.toast_rename_failed", {error: err})); this._render(); return; }
      await this._fetchState();
      this._showToast("ok", this._t("panel.toast_renamed"));
    }
  }

  // ---- Chores ----------------------------------------------------------
  async _doBulk(action, assigned_to) {
    const ids = [...this._bulkSel];
    if (!ids.length) return;
    if (action === "delete" && !confirm(this._t("panel.bulk_delete_confirm", { count: ids.length }))) return;
    const payload = { type: "taskmate/bulk_chore_action", action, chore_ids: ids };
    if (action === "reassign") payload.assigned_to = assigned_to || [];
    const { ok, err, res } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    this._bulkSel.clear();
    this._bulkMode = false;
    await this._fetchState();
    this._showToast("ok", this._t("panel.bulk_done_toast", { count: (res && res.count) || ids.length }));
  }

  async _doCloneChore(id) {
    if (!id) return;
    const { ok, err } = await this._callWS({ type: "taskmate/clone_chore", chore_id: id });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_chore_cloned"));
  }

  _openChoreDialog(id) {
    const blank = {
      name: "", description: "", points: 10,
      task_type: "standard",
      timed_rate_points: 10, timed_rate_minutes: 5, timed_max_daily_minutes: 0,
      assigned_to: [], requires_approval: true,
      time_category: "anytime", completion_sound: "coin", daily_limit: 1,
      difficulty: "medium",
      claim_allowance_minutes: 0,
      schedule_mode: "specific_days",
      due_days: [], recurrence: "weekly", recurrence_day: "", recurrence_start: "",
      first_occurrence_mode: "available_immediately",
      assignment_mode: "everyone", assignment_rotation_anchor: "",
      manual_start_child_id: "",
      require_availability: false,
      icon: "",
      visibility_entity: "", visibility_state: "on", visibility_operator: "none",
      weather_entity: "", weather_block_conditions: [],
      weather_temp_min: "", weather_temp_max: "", weather_wind_max: "",
      enabled: true,
      expires_on: "",
      due_time: "", early_bonus: 0, late_penalty: 0,
      mandatory: false, mandatory_penalty_points: 0,
      require_photo: false,
      publish_calendar_entities: [],
      depends_on: [],
      bonus_subtasks: [],
    };
    if (id) {
      const c = (this._state.chores || []).find(x => x.id === id);
      if (!c) return;
      this._openDialog({ kind: "chore", mode: "edit", data: {
        ...blank, ...c,
        assigned_to: [...(c.assigned_to || [])],
        due_days: [...(c.due_days || [])],
        publish_calendar_entities: [...(c.publish_calendar_entities || [])],
        depends_on: [...(c.depends_on || [])],
        bonus_subtasks: (c.bonus_subtasks || []).map(b => ({...b})),
        visibility_operator: c.visibility_operator || "none",
        weather_block_conditions: [...(c.weather_block_conditions || [])],
        // null/undefined limits render as an empty input, not "null"
        weather_temp_min: c.weather_temp_min ?? "",
        weather_temp_max: c.weather_temp_max ?? "",
        weather_wind_max: c.weather_wind_max ?? "",
        manual_start_child_id: "",
      } });
    } else {
      this._openDialog({ kind: "chore", mode: "add", data: blank });
    }
  }

  _pickChoreImage() {
    const input = document.createElement("input");
    input.type = "file";
    input.accept = "image/jpeg,image/png,image/webp";
    input.addEventListener("change", () => {
      const file = input.files && input.files[0];
      if (file) this._uploadChoreImage(file);
    });
    input.click();
  }

  // Downscale to 512px before upload: these render in slots between 20 and
  // 128px, so anything larger is bytes every device re-downloads for nothing.
  _downscaleChoreImage(file) {
    return new Promise((resolve, reject) => {
      const url = URL.createObjectURL(file);
      const img = new Image();
      img.onload = () => {
        const max = 512;
        let { width, height } = img;
        if (width > max || height > max) {
          const scale = Math.min(max / width, max / height);
          width = Math.round(width * scale);
          height = Math.round(height * scale);
        }
        const canvas = document.createElement("canvas");
        canvas.width = width;
        canvas.height = height;
        canvas.getContext("2d").drawImage(img, 0, 0, width, height);
        URL.revokeObjectURL(url);
        canvas.toBlob(b => (b ? resolve(b) : reject(new Error("encode failed"))), "image/jpeg", 0.8);
      };
      img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("decode failed")); };
      img.src = url;
    });
  }

  async _uploadChoreImage(file) {
    if (this._choreImageBusy) return;
    this._choreImageBusy = true;
    this._render();
    try {
      const blob = await this._downscaleChoreImage(file);
      const post = () => {
        const token = this._hass && this._hass.auth && this._hass.auth.data
          && this._hass.auth.data.access_token;
        const fd = new FormData();
        fd.append("file", blob, "chore.jpg");
        return fetch("/api/taskmate/image", {
          method: "POST",
          headers: token ? { Authorization: `Bearer ${token}` } : {},
          body: fd,
        });
      };
      // The cached access token can go stale mid-session, so refresh and retry
      // once on a 401 before giving up. Mirrors the child card's photo upload.
      if (this._hass && this._hass.auth && this._hass.auth.expired
        && this._hass.auth.refreshAccessToken) {
        try { await this._hass.auth.refreshAccessToken(); } catch (e) { /* surfaced below */ }
      }
      let resp = await post();
      if (resp.status === 401 && this._hass && this._hass.auth
        && this._hass.auth.refreshAccessToken) {
        await this._hass.auth.refreshAccessToken();
        resp = await post();
      }
      if (resp.status === 413) { this._showToast("err", this._t("panel.chore_image_too_large")); return; }
      if (resp.status === 400) { this._showToast("err", this._t("panel.chore_image_bad_type")); return; }
      if (!resp.ok) { this._showToast("err", this._t("panel.chore_image_failed")); return; }
      const body = await resp.json();
      if (this._dialog && body.image_url) this._dialog.data.image_url = body.image_url;
    } catch (err) {
      this._showToast("err", this._t("panel.chore_image_failed"));
    } finally {
      this._choreImageBusy = false;
      this._render();
    }
  }

  async _doSaveChore() {
    this._syncIconPickers();
    const d = this._dialog.data;
    if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_name_required")); return; }
    const wasAdd = this._dialog.mode === "add";
    const base = {
      name: d.name.trim(),
      description: d.description || "",
      icon: d.icon || "",
      image_url: d.image_url || "",
      points: Number(d.points) || 0,
      assigned_to: d.assigned_to || [],
      // The dialog tracks this via toggle-depends, but it was never sent, so a
      // dependency could neither be saved nor cleared — _ws_update_chore only
      // applies fields present in the message (#793).
      depends_on: d.depends_on || [],
      requires_approval: !!d.requires_approval,
      time_category: d.time_category || "anytime",
      claim_allowance_minutes: Math.max(0, Number(d.claim_allowance_minutes) || 0),
      completion_sound: d.completion_sound || "coin",
      difficulty: d.difficulty || "medium",
      daily_limit: Number(d.daily_limit) || 1,
      schedule_mode: d.schedule_mode || "specific_days",
      due_days: d.due_days || [],
      recurrence: d.recurrence || "weekly",
      recurrence_day: d.recurrence_day || "",
      recurrence_start: d.recurrence_start || "",
      first_occurrence_mode: d.first_occurrence_mode || "available_immediately",
      assignment_mode: d.assignment_mode || "everyone",
      assignment_rotation_anchor: d.assignment_rotation_anchor || "",
      require_availability: !!d.require_availability,
      visibility_entity: d.visibility_entity || "",
      visibility_state: d.visibility_state || "on",
      visibility_operator: d.visibility_operator || "none",
      weather_entity: d.weather_entity || "",
      weather_block_conditions: d.weather_block_conditions || [],
      weather_temp_min: this._optNum(d.weather_temp_min),
      weather_temp_max: this._optNum(d.weather_temp_max),
      weather_wind_max: this._optNum(d.weather_wind_max),
      enabled: d.enabled !== false,
      expires_on: (d.expires_on || "").trim(),
      due_time: (d.due_time || "").trim(),
      early_bonus: Math.max(0, Number(d.early_bonus) || 0),
      late_penalty: Math.max(0, Number(d.late_penalty) || 0),
      mandatory: !!d.mandatory,
      mandatory_penalty_points: Math.max(0, Number(d.mandatory_penalty_points) || 0),
      require_photo: !!d.require_photo,
      publish_calendar_entities: d.publish_calendar_entities || [],
      bonus_subtasks: (d.bonus_subtasks || []).filter(b => b.name && b.name.trim()).map(b => ({
        name: b.name.trim(), points: Number(b.points) || 5,
        description: b.description || "", ...(b.id ? {id: b.id} : {}),
      })),
      task_type: d.task_type || "standard",
      timed_rate_points: Number(d.timed_rate_points) || 10,
      timed_rate_minutes: Math.max(1, Number(d.timed_rate_minutes) || 5),
      timed_max_daily_minutes: Math.max(0, Number(d.timed_max_daily_minutes) || 0),
      manual_start_child_id: d.manual_start_child_id || null,
    };
    const payload = wasAdd
      ? { type: "taskmate/add_chore", ...base }
      : { type: "taskmate/update_chore", chore_id: d.id, ...base };
    const { ok, err } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", wasAdd ? this._t("panel.toast_chore_added") : this._t("panel.toast_chore_updated"));
  }

  _addBonusSubtask() {
    if (!this._dialog) return;
    const d = this._dialog.data;
    d.bonus_subtasks = d.bonus_subtasks || [];
    d.bonus_subtasks.push({ name: "", points: 5, description: "" });
    this._render();
  }

  _removeBonusSubtask(idx) {
    if (!this._dialog) return;
    const d = this._dialog.data;
    if (d.bonus_subtasks && d.bonus_subtasks[idx] !== undefined) {
      d.bonus_subtasks.splice(idx, 1);
      this._render();
    }
  }

  // ---- Rewards ---------------------------------------------------------
  _openRewardDialog(id) {
    const blank = { name: "", description: "", cost: 50, icon: "mdi:gift",
      assigned_to: [], is_jackpot: false, pool_enabled: false,
      quantity_str: "", expires_at: "",
      restock_enabled: false, restock_amount: 0, restock_period: "weekly",
      unlock_entity: "", unlock_minutes: 30 };
    if (id) {
      const r = (this._state.rewards || []).find(x => x.id === id);
      if (!r) return;
      this._openDialog({ kind: "reward", mode: "edit", data: {
        ...blank, ...r,
        assigned_to: [...(r.assigned_to || [])],
        quantity_str: r.quantity == null ? "" : String(r.quantity),
        expires_at: r.expires_at || "",
      } });
    } else {
      this._openDialog({ kind: "reward", mode: "add", data: blank });
    }
  }

  async _doSaveReward() {
    this._syncIconPickers();
    const d = this._dialog.data;
    if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_name_required")); return; }
    const wasAdd = this._dialog.mode === "add";
    const qty = (d.quantity_str === "" || d.quantity_str == null) ? null : Math.max(0, Number(d.quantity_str));
    const base = {
      name: d.name.trim(),
      cost: Number(d.cost) || 0,
      description: d.description || "",
      icon: d.icon || "mdi:gift",
      assigned_to: d.assigned_to || [],
      is_jackpot: !!d.is_jackpot,
      pool_enabled: !!d.pool_enabled,
      quantity: qty,
      expires_at: (d.expires_at || "").trim() || null,
      restock_enabled: !!d.restock_enabled,
      unlock_entity: d.unlock_entity || "",
      unlock_minutes: Math.max(0, Number(d.unlock_minutes) || 0),
      restock_amount: Math.max(0, Number(d.restock_amount) || 0),
      restock_period: d.restock_period || "weekly",
    };
    const payload = wasAdd ? { type: "taskmate/add_reward", ...base } : { type: "taskmate/update_reward", reward_id: d.id, ...base };
    const { ok, err } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", wasAdd ? this._t("panel.toast_reward_added") : this._t("panel.toast_reward_updated"));
  }

  // ---- Quests (chore chains) -------------------------------------------
  _openQuestDialog(id) {
    const blank = { name: "", description: "", icon: "mdi:map-marker-path",
      steps: [], bonus_points: 25, assigned_to: [], repeatable: false, active: true };
    if (id) {
      const q = (this._state.quests || []).find(x => x.id === id);
      if (!q) return;
      this._openDialog({ kind: "quest", mode: "edit", data: {
        ...blank, ...q,
        steps: [...(q.steps || [])],
        assigned_to: [...(q.assigned_to || [])],
      } });
    } else {
      this._openDialog({ kind: "quest", mode: "add", data: blank });
    }
  }

  _moveQuestStep(idx, dir) {
    if (!this._dialog || !this._dialog.data) return;
    const arr = [...(this._dialog.data.steps || [])];
    const j = dir === "up" ? idx - 1 : idx + 1;
    if (idx < 0 || idx >= arr.length || j < 0 || j >= arr.length) return;
    [arr[idx], arr[j]] = [arr[j], arr[idx]];
    this._dialog.data.steps = arr;
    this._render();
  }

  async _doSaveQuest() {
    this._syncIconPickers();
    const d = this._dialog.data;
    if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_name_required")); return; }
    if (!d.steps || d.steps.length === 0) { this._showToast("err", this._t("panel.quest_need_step")); return; }
    const wasAdd = this._dialog.mode === "add";
    const base = {
      name: d.name.trim(),
      description: d.description || "",
      icon: d.icon || "mdi:map-marker-path",
      steps: [...d.steps],
      bonus_points: Math.max(0, Number(d.bonus_points) || 0),
      assigned_to: d.assigned_to || [],
      repeatable: !!d.repeatable,
      active: d.active !== false,
    };
    const payload = wasAdd ? { type: "taskmate/create_quest", ...base } : { type: "taskmate/update_quest", quest_id: d.id, ...base };
    const { ok, err } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", wasAdd ? this._t("panel.toast_quest_added") : this._t("panel.toast_quest_updated"));
  }

  // ---- Challenges (daily / weekly) -------------------------------------
  _openChallengeDialog(id) {
    const blank = { name: "", description: "", icon: "mdi:trophy-outline",
      scope: "daily", metric: "chores", target: 3, bonus_points: 15,
      assigned_to: [], active: true };
    if (id) {
      const c = (this._state.challenges || []).find(x => x.id === id);
      if (!c) return;
      this._openDialog({ kind: "challenge", mode: "edit", data: {
        ...blank, ...c, assigned_to: [...(c.assigned_to || [])],
      } });
    } else {
      this._openDialog({ kind: "challenge", mode: "add", data: blank });
    }
  }

  async _doSaveChallenge() {
    this._syncIconPickers();
    const d = this._dialog.data;
    if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_name_required")); return; }
    const target = Math.max(1, Number(d.target) || 0);
    const wasAdd = this._dialog.mode === "add";
    const base = {
      name: d.name.trim(),
      description: d.description || "",
      icon: d.icon || "mdi:trophy-outline",
      scope: d.scope || "daily",
      metric: d.metric || "chores",
      target,
      bonus_points: Math.max(0, Number(d.bonus_points) || 0),
      assigned_to: d.assigned_to || [],
      active: d.active !== false,
    };
    const payload = wasAdd ? { type: "taskmate/create_challenge", ...base } : { type: "taskmate/update_challenge", challenge_id: d.id, ...base };
    const { ok, err } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", wasAdd ? this._t("panel.toast_challenge_added") : this._t("panel.toast_challenge_updated"));
  }

  _renderChallengeDialog() {
    const d = this._dialog.data;
    const children = this._state.children || [];
    const pointsName = this._state.settings.points_name || this._t("common.points");
    return this._dialogShell(this._dialog.mode === "add" ? this._t("panel.dialog_add_challenge") : this._t("panel.dialog_edit_challenge"),
      [
        this._field(this._t("panel.chore_name_label"), "name", d.name, "text"),
        this._field(this._t("panel.chore_description_label"), "description", d.description, "text"),
        `<div class="tm-field-row">
          ${this._select(this._t("panel.challenge_scope_label"), "scope", d.scope || "daily", [
            { v: "daily", l: this._t("panel.challenge_scope_daily") },
            { v: "weekly", l: this._t("panel.challenge_scope_weekly") },
          ])}
          ${this._select(this._t("panel.challenge_metric_label"), "metric", d.metric || "chores", [
            { v: "chores", l: this._t("panel.challenge_metric_chores") },
            { v: "points", l: this._t("panel.challenge_metric_points", {points_name: pointsName}) },
          ])}
        </div>`,
        `<div class="tm-field-row">
          ${this._field(this._t("panel.challenge_target_label"), "target", d.target, "number", this._t("panel.challenge_target_hint"))}
          ${this._field(this._t("panel.quest_bonus_label", {points_name: pointsName}), "bonus_points", d.bonus_points, "number")}
        </div>`,
        this._iconPickerField(this._t("panel.reward_icon_label"), "icon", d.icon),
        children.length > 0 ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.quest_assigned_label")}</span>
            <div class="tm-chip-row">
              ${children.map(c => `
                <button type="button" class="tm-chip-btn ${(d.assigned_to || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-challenge-assigned" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
            <span class="tm-field-hint">${this._t("panel.quest_assigned_hint")}</span>
          </div>
        ` : "",
        this._switch(this._t("panel.quest_active_label"), "active", d.active !== false,
          this._t("panel.quest_active_hint")),
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-challenge">${this._t("panel.btn_save")}</button>`
    );
  }

  // ---- Avatar unlockables ----------------------------------------------
  _openAvatarCatalogDialog() {
    const cat = (this._state.avatar_catalog || []).map(a => ({
      id: a.id || a.icon, label: a.label || "", icon: a.icon || "mdi:account-circle",
      unlock_type: a.unlock_type || "free", unlock_value: Number(a.unlock_value) || 0,
    }));
    this._openDialog({ kind: "avatar-catalog", mode: "edit", data: { catalog: cat } });
  }

  _avatarAddRow() {
    if (!this._dialog || !this._dialog.data) return;
    this._dialog.data.catalog = [...(this._dialog.data.catalog || []), {
      id: "", label: "", icon: "mdi:account-circle", unlock_type: "free", unlock_value: 0,
    }];
    this._render();
  }

  _avatarDelRow(idx) {
    if (!this._dialog || !this._dialog.data) return;
    const arr = [...(this._dialog.data.catalog || [])];
    if (idx < 0 || idx >= arr.length) return;
    arr.splice(idx, 1);
    this._dialog.data.catalog = arr;
    this._render();
  }

  async _doSaveAvatarCatalog() {
    this._syncIconPickers();
    const cat = (this._dialog.data.catalog || [])
      .filter(a => (a.icon || "").trim())
      .map(a => ({
        id: (a.id || a.icon).trim(),
        label: (a.label || "").trim(),
        icon: a.icon.trim(),
        unlock_type: a.unlock_type || "free",
        unlock_value: Math.max(0, Number(a.unlock_value) || 0),
      }));
    const { ok, err } = await this._callWS({ type: "taskmate/update_avatar_catalog", catalog: cat });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_avatars_saved"));
  }

  // ---- Penalties / Bonuses ---------------------------------------------
  _openPenBonDialog(kind, id) {
    const blank = { name: "", description: "", points: 10,
      icon: kind === "penalty" ? "mdi:alert-circle-outline" : "mdi:star-circle-outline",
      assigned_to: [] };
    if (id) {
      const item = ((kind === "penalty" ? this._state.penalties : this._state.bonuses) || []).find(x => x.id === id);
      if (!item) return;
      this._openDialog({ kind, mode: "edit", data: { ...blank, ...item, assigned_to: [...(item.assigned_to || [])] } });
    } else {
      this._openDialog({ kind, mode: "add", data: blank });
    }
  }

  async _doSavePenBon(kind) {
    this._syncIconPickers();
    const d = this._dialog.data;
    if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_name_required")); return; }
    if (!d.points || d.points < 1) { this._showToast("err", this._t("panel.toast_points_required")); return; }
    const wasAdd = this._dialog.mode === "add";
    const wsBase = kind === "penalty" ? "penalty" : "bonus";
    const idField = `${wsBase}_id`;
    const wsType = wasAdd ? `taskmate/add_${wsBase}` : `taskmate/update_${wsBase}`;
    const base = {
      name: d.name.trim(),
      points: Number(d.points),
      description: d.description || "",
      icon: d.icon || (kind === "penalty" ? "mdi:alert-circle-outline" : "mdi:star-circle-outline"),
      assigned_to: d.assigned_to || [],
    };
    const payload = wasAdd ? { type: wsType, ...base } : { type: wsType, [idField]: d.id, ...base };
    const { ok, err } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", this._t(`panel.toast_${wasAdd ? "added" : "updated"}_${kind}`));
  }

  _openApplyDialog(kind, id) {
    const item = ((kind === "penalty" ? this._state.penalties : this._state.bonuses) || []).find(x => x.id === id);
    if (!item) return;
    this._openDialog({ kind: `apply-${kind}`, mode: "apply", data: { id, item } });
  }

  async _doApplyPenBon(kind, id, child_id) {
    const wsType = kind === "penalty" ? "taskmate/apply_penalty" : "taskmate/apply_bonus";
    const idField = `${kind}_id`;
    const { ok, err } = await this._callWS({ type: wsType, [idField]: id, child_id });
    if (!ok) { this._showToast("err", this._t("panel.toast_apply_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", this._t(kind === "penalty" ? "panel.toast_penalty_applied" : "panel.toast_bonus_applied"));
  }

  // ---- Task groups -----------------------------------------------------
  _openGroupDialog(id) {
    const blank = { name: "", policy: "sticky", chore_ids: [] };
    if (id) {
      const g = (this._state.task_groups || []).find(x => x.id === id);
      if (!g) return;
      this._openDialog({ kind: "group", mode: "edit", data: { ...blank, ...g, chore_ids: [...(g.chore_ids || [])] } });
    } else {
      this._openDialog({ kind: "group", mode: "add", data: blank });
    }
  }

  async _doSaveGroup() {
    const d = this._dialog.data;
    if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_name_required")); return; }
    const wasAdd = this._dialog.mode === "add";
    const base = { name: d.name.trim(), policy: d.policy || "sticky", chore_ids: d.chore_ids || [] };
    const payload = wasAdd ? { type: "taskmate/add_task_group", ...base } : { type: "taskmate/update_task_group", group_id: d.id, ...base };
    const { ok, err } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", wasAdd ? this._t("panel.toast_group_added") : this._t("panel.toast_group_updated"));
  }

  // ---- Time-of-day periods ----------------------------------------------
  _defaultTimePeriods() {
    return [
      { id: "morning",   label: "", start: "06:00", end: "12:00", icon: "mdi:weather-sunny" },
      { id: "afternoon", label: "", start: "12:00", end: "17:00", icon: "mdi:white-balance-sunny" },
      { id: "evening",   label: "", start: "17:00", end: "21:00", icon: "mdi:weather-sunset" },
      { id: "night",     label: "", start: "21:00", end: "23:59", icon: "mdi:weather-night" },
    ];
  }

  // Saved periods, falling back to the legacy flat keys / built-in defaults
  // for installs that haven't saved through the new editor yet.
  _effectiveTimePeriods() {
    const s = (this._state && this._state.settings) || {};
    if (Array.isArray(s.time_periods) && s.time_periods.length) {
      return s.time_periods.map(p => ({ ...p }));
    }
    return this._defaultTimePeriods().map(p => ({
      ...p,
      start: s[`time_${p.id}_start`] || p.start,
      end: s[`time_${p.id}_end`] || p.end,
    }));
  }

  _isBuiltinPeriod(id) {
    return ["morning", "afternoon", "evening", "night"].includes(id);
  }

  _timePeriodLabel(p) {
    if (p.label) return p.label;
    return this._isBuiltinPeriod(p.id) ? this._t(`panel.time_${p.id}`) : (p.id || "");
  }

  _timeCategoryOptions() {
    const opts = [{ v: "anytime", l: this._t("panel.time_anytime") }];
    for (const p of this._effectiveTimePeriods()) opts.push({ v: p.id, l: this._timePeriodLabel(p) });
    return opts;
  }

  _timeCategoryLabel(cat) {
    const c = cat || "anytime";
    if (c === "anytime") return this._t("panel.time_anytime");
    const p = this._effectiveTimePeriods().find(x => x.id === c);
    return p ? this._timePeriodLabel(p) : c;
  }

  _renderTimePeriodsSection() {
    if (!this._timePeriodsDraft) this._timePeriodsDraft = this._effectiveTimePeriods();
    const inUse = {};
    (this._state.chores || []).forEach(c => {
      const tc = c.time_category;
      if (tc && tc !== "anytime") inUse[tc] = (inUse[tc] || 0) + 1;
    });
    const rows = this._timePeriodsDraft.map((p, i) => {
      const used = p.id ? inUse[p.id] || 0 : 0;
      const placeholder = this._isBuiltinPeriod(p.id) ? this._t(`panel.time_${p.id}`) : this._t("panel.tp_label_placeholder");
      return `
            <div class="tm-setting-row tm-period-row">
              <ha-icon-picker data-tp-field="icon" data-tp-idx="${i}" data-current="${this._esc(p.icon || "mdi:clock-outline")}"></ha-icon-picker>
              <input type="text" class="tm-input" data-tp-field="label" data-tp-idx="${i}" value="${this._esc(p.label || "")}" placeholder="${this._esc(placeholder)}">
              <div class="tm-time-pair">
                <input type="time" class="tm-input" data-tp-field="start" data-tp-idx="${i}" value="${this._esc(p.start || "")}">
                <span>–</span>
                <input type="time" class="tm-input" data-tp-field="end" data-tp-idx="${i}" value="${this._esc(p.end || "")}">
              </div>
              ${used
                ? `<button type="button" class="tm-icon-btn tm-period-del" disabled title="${this._esc(this._t("panel.tp_in_use_title", { count: used }))}"><ha-icon icon="mdi:lock-outline"></ha-icon></button>`
                : `<button type="button" class="tm-icon-btn tm-period-del" data-act="tp-remove" data-idx="${i}" title="${this._esc(this._t("panel.tp_delete_title"))}"><ha-icon icon="mdi:trash-can-outline"></ha-icon></button>`}
              ${used ? `<div class="tm-period-inuse">${this._esc(this._t("panel.tp_in_use_hint", { count: used }))}</div>` : ""}
            </div>`;
    }).join("");
    return `
        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.settings_time_boundaries_title")}</h3><p class="tm-meta">${this._t("panel.settings_time_boundaries_hint")}</p></div></div>
          <div class="tm-section-body">
            ${rows}
            <div class="tm-setting-row tm-period-row tm-period-anytime">
              <ha-icon icon="mdi:clock-outline"></ha-icon>
              <div class="tm-setting-label">${this._t("panel.time_anytime")}<small>${this._t("panel.tp_anytime_hint")}</small></div>
            </div>
            <div class="tm-period-actions">
              <button type="button" class="tm-btn" data-act="tp-add"><ha-icon icon="mdi:plus"></ha-icon> ${this._t("panel.tp_add_period")}</button>
              <button type="button" class="tm-btn" data-act="tp-reset">${this._t("panel.tp_reset_defaults")}</button>
            </div>
          </div>
        </div>`;
  }

  // Pull current editor input values into the draft. Inputs also stream into
  // the draft via input/value-changed events; this is the safety net before
  // any re-render or save.
  _syncTimePeriodInputs() {
    if (!this._timePeriodsDraft) return;
    this.querySelectorAll("[data-tp-field]").forEach(el => {
      const idx = Number(el.dataset.tpIdx);
      const field = el.dataset.tpField;
      if (this._timePeriodsDraft[idx] && el.value != null) {
        this._timePeriodsDraft[idx][field] = el.value;
      }
    });
  }

  _renderVacationSection() {
    if (!this._vacationDraft) {
      const saved = (this._state?.settings?.vacation_periods) || [];
      this._vacationDraft = saved.map(p => ({ id: p.id || "", name: p.name || "", start: p.start || "", end: p.end || "" }));
    }
    const rows = this._vacationDraft.map((p, i) => `
            <div class="tm-setting-row tm-vacation-row">
              <input type="text" class="tm-input" data-vac-field="name" data-vac-idx="${i}" value="${this._esc(p.name || "")}" placeholder="${this._esc(this._t("panel.vacation_name_placeholder"))}">
              <div class="tm-time-pair">
                <input type="date" class="tm-input" data-vac-field="start" data-vac-idx="${i}" value="${this._esc(p.start || "")}">
                <span>–</span>
                <input type="date" class="tm-input" data-vac-field="end" data-vac-idx="${i}" value="${this._esc(p.end || "")}">
              </div>
              <button type="button" class="tm-icon-btn tm-period-del" data-act="vac-remove" data-idx="${i}" title="${this._esc(this._t("panel.vacation_delete_title"))}"><ha-icon icon="mdi:trash-can-outline"></ha-icon></button>
            </div>`).join("");
    const calValue = this._settingsEntityDraft?.vacation_calendar
      ?? this._state?.settings?.vacation_calendar ?? "";
    return `
        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.settings_vacation_title")}</h3><p class="tm-meta">${this._t("panel.settings_vacation_hint")}</p></div></div>
          <div class="tm-section-body">
            ${rows || `<div class="tm-meta" style="padding: 4px 20px 8px;">${this._t("panel.vacation_none")}</div>`}
            <div class="tm-period-actions">
              <button type="button" class="tm-btn" data-act="vac-add"><ha-icon icon="mdi:plus"></ha-icon> ${this._t("panel.vacation_add")}</button>
            </div>
            <div style="padding: 12px 20px 4px;">
              ${this._entityPickerField(this._t("panel.vacation_calendar_label"), "vacation_calendar", calValue, ["calendar"], this._t("panel.vacation_calendar_hint"))}
            </div>
          </div>
        </div>`;
  }

  _syncVacationInputs() {
    if (!this._vacationDraft) return;
    this.querySelectorAll("[data-vac-field]").forEach(el => {
      const idx = Number(el.dataset.vacIdx);
      const field = el.dataset.vacField;
      if (this._vacationDraft[idx] && el.value != null) this._vacationDraft[idx][field] = el.value;
    });
  }

  _validateTimePeriodsDraft() {
    const periods = [];
    for (const p of this._timePeriodsDraft) {
      const label = (p.label || "").trim();
      const id = (p.id || "").trim();
      const name = label || (this._isBuiltinPeriod(id) ? this._t(`panel.time_${id}`) : id);
      if (!label && !this._isBuiltinPeriod(id)) return { error: this._t("panel.tp_err_label_required") };
      if (!/^\d{2}:\d{2}$/.test(p.start || "") || !/^\d{2}:\d{2}$/.test(p.end || "")) {
        return { error: this._t("panel.tp_err_time_required", { name }) };
      }
      if (p.start >= p.end) return { error: this._t("panel.tp_err_order", { name }) };
      periods.push({ id, label, start: p.start, end: p.end, icon: p.icon || "" });
    }
    if (!periods.length) return { error: this._t("panel.tp_err_empty") };
    periods.sort((a, b) => (a.start < b.start ? -1 : 1));
    for (let i = 1; i < periods.length; i++) {
      if (periods[i].start < periods[i - 1].end) {
        const nameOf = (q) => q.label || (this._isBuiltinPeriod(q.id) ? this._t(`panel.time_${q.id}`) : q.id);
        return { error: this._t("panel.tp_err_overlap", { a: nameOf(periods[i]), b: nameOf(periods[i - 1]) }) };
      }
    }
    return { periods };
  }

  // ---- Backup / restore ------------------------------------------------
  async _doExportConfig() {
    const { ok, err, res } = await this._callWS({ type: "taskmate/config/export" });
    if (!ok || !res) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    try {
      const blob = new Blob([JSON.stringify(res, null, 2)], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "taskmate-backup.json";
      a.click();
      URL.revokeObjectURL(url);
      this._showToast("ok", this._t("panel.backup_exported"));
    } catch (e) {
      this._showToast("err", String(e));
    }
  }

  async _icsShow() {
    const { ok, err, res } = await this._callWS({ type: "taskmate/calendar/get_ics_url" });
    if (!ok || !res) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    this._icsUrl = res.url;
    this._render();
  }

  async _icsRegenerate() {
    const { ok, err, res } = await this._callWS({ type: "taskmate/calendar/regenerate_ics_token" });
    if (!ok || !res) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    this._icsUrl = res.url;
    this._render();
    this._showToast("ok", this._t("panel.ics_regenerated"));
  }

  async _icsCopy() {
    const input = this.querySelector("[data-role='ics-url']");
    if (!input) return;
    try {
      await navigator.clipboard.writeText(input.value);
      this._showToast("ok", this._t("panel.ics_copied"));
    } catch (e) {
      input.select();
      this._showToast("err", String(e));
    }
  }

  async _doImportConfig(file) {
    let payload;
    try {
      payload = JSON.parse(await file.text());
    } catch (e) {
      this._showToast("err", this._t("panel.backup_import_bad_file"));
      return;
    }
    if (!payload || typeof payload !== "object" || !payload.data) {
      this._showToast("err", this._t("panel.backup_import_bad_file"));
      return;
    }
    if (!confirm(this._t("panel.backup_import_confirm"))) return;
    const { ok, err } = await this._callWS({ type: "taskmate/config/import", payload });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.backup_imported"));
  }

  // ---- Settings --------------------------------------------------------
  async _doSaveSettings() {
    const root = this.querySelector(".tm-settings");
    if (!root) return;
    const fields = root.querySelectorAll("[data-setting]");
    const payload = { type: "taskmate/update_settings" };
    fields.forEach(el => {
      const name = el.dataset.setting;
      const declaredType = el.getAttribute("type") || el.type;
      let v;
      if (declaredType === "checkbox" || el.tagName === "HA-SWITCH") v = el.checked;
      else if (declaredType === "number") v = el.value === "" ? undefined : Number(el.value);
      else v = el.value;
      if (v !== undefined) payload[name] = v;
    });
    root.querySelectorAll("ha-icon-picker[data-setting]").forEach(el => {
      payload[el.dataset.setting] = el.value || "";
    });
    // Non-admin parent role (#661): collect the ticked HA users.
    const parentBoxes = root.querySelectorAll("input[type=checkbox][data-parent-user]");
    if (parentBoxes.length) {
      payload.parent_user_ids = Array.from(parentBoxes)
        .filter(el => el.checked)
        .map(el => el.dataset.parentUser);
    }
    if (this._settingsEntityDraft) {
      Object.assign(payload, this._settingsEntityDraft);
    }
    if (this._timePeriodsDraft) {
      this._syncTimePeriodInputs();
      const { periods, error } = this._validateTimePeriodsDraft();
      if (error) { this._showToast("err", error); return; }
      payload.time_periods = periods;
    }
    if (this._vacationDraft) {
      this._syncVacationInputs();
      const vacs = [];
      for (const p of this._vacationDraft) {
        const name = (p.name || "").trim();
        const start = p.start || "";
        const end = p.end || "";
        if (!start && !end && !name) continue;  // skip wholly-empty rows
        if (!start || !end) { this._showToast("err", this._t("panel.vacation_dates_required")); return; }
        vacs.push({ ...(p.id ? { id: p.id } : {}), name, start, end });
      }
      payload.vacation_periods = vacs;
    }
    const { ok, err, res } = await this._callWS(payload);
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", {error: err})); return; }
    this._timePeriodsDraft = null;  // rebuild from saved state on next render
    this._vacationDraft = null;
    this._settingsEntityDraft = null;
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_settings_saved", {count: (res && res.updated || []).length}));
  }

  // ---- Notifications WS helpers ----------------------------------------
  async _notifSetMaster(typeId, enabled) {
    await this._callWS({ type: "taskmate/notifications/set_master_enabled", type_id: typeId, enabled });
    await this._fetchState();
  }

  async _doSendTestNotif(typeId) {
    const { ok, err, res } = await this._callWS({ type: "taskmate/notifications/send_test", type_id: typeId });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    const count = (res && res.sent || []).length;
    this._showToast("ok", this._t("panel.notif_test_sent", { count }));
  }

  async _notifSetRoute(typeId, recipientId, enabled, time) {
    await this._callWS({ type: "taskmate/notifications/set_route", type_id: typeId, recipient_id: recipientId, enabled, time });
    await this._fetchState();
  }

  async _notifSetChildNotify(childId, notifyService) {
    await this._callWS({ type: "taskmate/notifications/set_child_notify", child_id: childId, notify_service: notifyService || null });
    await this._fetchState();
  }

  async _notifSetChildQuiet(childId, start, end) {
    await this._callWS({ type: "taskmate/notifications/set_child_quiet", child_id: childId, quiet_hours_start: start || "", quiet_hours_end: end || "" });
    await this._fetchState();
  }

  async _notifSetStreakCutoff(time) {
    await this._callWS({ type: "taskmate/notifications/set_streak_cutoff", time });
    await this._fetchState();
  }

  async _notifSetNavUrl(typeId, navUrl) {
    const payload = { type: "taskmate/notifications/set_nav_url", nav_url: navUrl || "" };
    if (typeId) payload.type_id = typeId;
    const { ok, err } = await this._callWS(payload);
    if (!ok) this._showToast("err", this._t("panel.toast_save_failed", { error: err }));
    await this._fetchState();
  }

  async _notifSetGroup(typeId, group) {
    const payload = { type: "taskmate/notifications/set_group", group: group || "" };
    if (typeId) payload.type_id = typeId;
    const { ok, err } = await this._callWS(payload);
    if (!ok) this._showToast("err", this._t("panel.toast_save_failed", { error: err }));
    await this._fetchState();
  }

  async _notifSetEscalation(field, value) {
    const s = (this._notifState && this._notifState.settings) || {};
    const cur = {
      reminder_minutes: Number(s.mandatory_escalation_reminder_minutes) || 30,
      parent_minutes: Number(s.mandatory_escalation_parent_minutes) || 120,
    };
    const n = Math.max(1, Math.min(1440, Math.round(Number(value) || cur[field])));
    cur[field] = n;
    await this._callWS({ type: "taskmate/notifications/set_escalation", reminder_minutes: cur.reminder_minutes, parent_minutes: cur.parent_minutes });
    await this._fetchState();
  }

  async _notifUpsertParent(p) {
    const payload = { type: "taskmate/notifications/upsert_parent", name: p.name, notify_service: p.notify_service, enabled: p.enabled !== false };
    if (p.id) payload.parent_id = p.id;
    await this._callWS(payload);
    await this._fetchState();
  }

  async _notifAddParent() {
    const first = (this._notifyServices || [])[0] || "";
    await this._notifUpsertParent({ name: this._t("panel.notif_default_parent_name"), notify_service: first, enabled: true });
  }

  async _notifDeleteParent(parentId) {
    await this._callWS({ type: "taskmate/notifications/delete_parent", parent_id: parentId });
    await this._fetchState();
  }

  async _notifUpsertCustom(n) {
    const payload = {
      type: "taskmate/notifications/upsert_custom",
      name: n.name, message_template: n.message_template, time: n.time,
      day_mask: n.day_mask, recipient_ids: n.recipient_ids, enabled: n.enabled,
    };
    if (n.id) payload.custom_id = n.id;
    await this._callWS(payload);
    await this._fetchState();
  }

  async _notifAddCustom() {
    await this._notifUpsertCustom({
      name: this._t("panel.notif_default_custom_name"),
      message_template: this._t("panel.notif_default_custom_message"),
      time: "20:00",
      day_mask: 0b1111111,
      recipient_ids: [],
      enabled: true,
    });
  }

  async _notifDeleteCustom(customId) {
    await this._callWS({ type: "taskmate/notifications/delete_custom", custom_id: customId });
    await this._fetchState();
  }

  async _notifToggleCustom(customId, enabled) {
    const n = (this._notifState && this._notifState.custom || []).find(x => x.id === customId);
    if (!n) return;
    await this._notifUpsertCustom({ ...n, enabled });
  }

  async _notifToggleDay(customId, dayIdx, isOn) {
    const n = (this._notifState && this._notifState.custom || []).find(x => x.id === customId);
    if (!n) return;
    const bit = 1 << dayIdx;
    const newMask = isOn ? (n.day_mask | bit) : (n.day_mask & ~bit);
    await this._notifUpsertCustom({ ...n, day_mask: newMask });
  }

  async _notifToggleRecipient(customId, recipientId) {
    const n = (this._notifState && this._notifState.custom || []).find(x => x.id === customId);
    if (!n) return;
    const has = n.recipient_ids.includes(recipientId);
    const newIds = has ? n.recipient_ids.filter(x => x !== recipientId) : [...n.recipient_ids, recipientId];
    await this._notifUpsertCustom({ ...n, recipient_ids: newIds });
  }

  async _notifUpdateCustomField(customId, field, value) {
    const n = (this._notifState && this._notifState.custom || []).find(x => x.id === customId);
    if (!n) return;
    await this._notifUpsertCustom({ ...n, [field]: value });
  }

  // ---- HA picker binding -----------------------------------------------
  _bindHaPickers(syncValues = true) {
    if (!this._hass) return;
    this.querySelectorAll("ha-icon-picker").forEach(el => {
      el.hass = this._hass;
      if (syncValues) {
        const v = el.getAttribute("data-current") || "";
        if (el.value !== v) el.value = v;
      }
    });
    if (!syncValues) return;
    this.querySelectorAll("ha-textfield[data-value]").forEach(el => {
      const v = el.getAttribute("data-value") || "";
      if (el.value !== v) el.value = v;
    });
    this.querySelectorAll("ha-select[data-value], select[data-value]").forEach(el => {
      const v = el.getAttribute("data-value") || "";
      if (el.value !== v) el.value = v;
    });
  }

  // ---- Time helpers ----------------------------------------------------
  _timeAgo(iso) {
    if (!iso) return "—";
    const then = new Date(iso);
    if (isNaN(then)) return iso;
    const diffSec = Math.floor((Date.now() - then.getTime()) / 1000);
    if (diffSec < 60)         return this._t("panel.time_ago_just_now");
    if (diffSec < 3600)       return this._t("panel.time_ago_minutes", {count: Math.floor(diffSec / 60)});
    if (diffSec < 86400)      return this._t("panel.time_ago_hours", {count: Math.floor(diffSec / 3600)});
    if (diffSec < 604800)     return this._t("panel.time_ago_days", {count: Math.floor(diffSec / 86400)});
    return then.toLocaleDateString();
  }

  // ---- rendering -------------------------------------------------------
  _render() {
    this._rendered = true;
    // A re-render rebuilds the rows the floating row menu is anchored to, so
    // drop any open menu rather than leave it orphaned/mispositioned.
    if (this._rowMenuEl) this._closeRowMenu();

    // Self-heal: HA's partial-panel-resolver removes the panel element from
    // the DOM while the WS connection is down (e.g. a long-backgrounded tab)
    // and re-attaches it on reconnect. If our shell was stripped out, force a
    // full rebuild — otherwise the zone-diff below short-circuits on the
    // stale _zoneCache and the content area stays blank until a hard reload.
    if (this._shellReady && !this.querySelector(".tm-shell")) {
      this._shellReady = false;
      this._zoneCache = {};
    }

    if (!this._shellReady) {
      if (!this._cachedStyles) {
        this._cachedStyles = this._styles();
      }
      const existingToast = this.querySelector(".tm-toast");
      this.innerHTML = `
        ${this._cachedStyles}
        <div class="tm-shell">
          <div data-zone="sidebar">${this._sidebar()}</div>
          <div class="tm-main">
            <div data-zone="topbar">${this._topbar()}</div>
            <div data-zone="mtabs">${this._mobileTabs()}</div>
            <div data-zone="approval">${this._approvalBanner()}</div>
            <div class="tm-body">
              <div class="tm-body-inner" data-zone="body">${this._renderBody()}</div>
            </div>
            <div data-zone="dialog">${this._dialog ? this._renderDialog() : ""}</div>
            <div data-zone="tpl">${this._renderSaveTemplateDialog()}</div>
          </div>
        </div>
      `;
      if (existingToast) this.appendChild(existingToast);
      this._shellReady = true;
      this._zoneCache = {};
      this._applyDesign();
      this._bindHaPickers();
      return;
    }

    const zones = {
      sidebar:  this._sidebar(),
      topbar:   this._topbar(),
      mtabs:    this._mobileTabs(),
      approval: this._approvalBanner(),
      body:     this._renderBody(),
      dialog:   this._dialog ? this._renderDialog() : "",
      tpl:      this._renderSaveTemplateDialog(),
    };

    const dialogZone = this.querySelector('[data-zone="dialog"]');
    let savedScroll = 0;
    if (dialogZone) {
      const dialogBody = dialogZone.querySelector(".tm-dialog-body");
      savedScroll = dialogBody ? dialogBody.scrollTop : 0;
    }

    if (this._dialog) {
      const openSet = this._dialog._openAdvanced instanceof Set ? this._dialog._openAdvanced : new Set();
      this.querySelectorAll("details.tm-advanced[data-section]").forEach(el => {
        const key = el.dataset.section;
        if (!key) return;
        if (el.open) openSet.add(key); else openSet.delete(key);
      });
      this._dialog._openAdvanced = openSet;
    }

    let anyChanged = false;
    for (const [name, html] of Object.entries(zones)) {
      if (this._zoneCache[name] === html) continue;
      this._zoneCache[name] = html;
      const el = this.querySelector(`[data-zone="${name}"]`);
      if (!el) { this._shellReady = false; this._render(); return; }
      el.innerHTML = html;
      anyChanged = true;
    }

    if (anyChanged) {
      this._bindHaPickers();
    }

    if (savedScroll) {
      const newBody = dialogZone && dialogZone.querySelector(".tm-dialog-body");
      if (newBody) newBody.scrollTop = savedScroll;
    }

    this._applyDesign();
  }

  // The admin panel intentionally stays CLASSIC regardless of the global card
  // design (the per-card design styles apply to Lovelace cards only). This
  // clears any stale attribute so no designed rules can attach.
  _applyDesign() {
    const shell = this.querySelector(".tm-shell");
    if (shell) shell.removeAttribute("data-tm-design");
  }

  _sidebarGroups() {
    const counts = this._state ? {
      children:  (this._state.children || []).length,
      activity:  (this._state.pending_completions || []).length + (this._state.pending_reward_claims || []).length + (this._state.swap_requests || []).length,
      chores:    (this._state.chores || []).length,
      rewards:   (this._state.rewards || []).length,
      penalties: (this._state.penalties || []).length,
      bonuses:   (this._state.bonuses || []).length,
      groups:    (this._state.task_groups || []).length,
      templates: (this._state.templates || []).length,
    } : {};
    return [
      { head: this._t("panel.nav_today"), items: [
        { id: "activity", label: this._t("panel.tab_activity"), icon: "mdi:pulse" },
        { id: "insights", label: this._t("panel.tab_insights"), icon: "mdi:chart-box-outline" },
      ]},
      { head: this._t("panel.nav_manage"), items: [
        { id: "children",  label: this._t("panel.tab_children"),  icon: "mdi:account-multiple" },
        { id: "chores",    label: this._t("panel.tab_chores"),    icon: "mdi:check-circle-outline" },
        { id: "rewards",   label: this._t("panel.tab_rewards"),   icon: "mdi:gift-outline" },
        { id: "penalties", label: this._t("panel.tab_penalties"), icon: "mdi:alert-circle-outline" },
        { id: "bonuses",   label: this._t("panel.tab_bonuses"),   icon: "mdi:flash-outline" },
        { id: "groups",    label: this._t("panel.tab_groups"),    icon: "mdi:layers-outline" },
        { id: "quests",    label: this._t("panel.tab_quests"),    icon: "mdi:map-marker-path" },
        { id: "challenges", label: this._t("panel.tab_challenges"), icon: "mdi:trophy-outline" },
        { id: "badges",    label: this._t("panel.tab_badges"),     icon: "mdi:medal-outline" },
        { id: "templates", label: this._t("panel.tab_templates"), icon: "mdi:clipboard-list-outline" },
      ]},
      { head: this._t("panel.nav_system"), items: [
        { id: "notifications", label: this._t("panel.tab_notifications"), icon: "mdi:bell-outline" },
        // The audit view has always been implemented (and documented in the
        // wiki), but it was never listed here — and the nav is built solely
        // from this list, so there was no way to open it.
        { id: "audit", label: this._t("panel.tab_audit"), icon: "mdi:history" },
        { id: "settings", label: this._t("panel.tab_settings"), icon: "mdi:cog-outline" },
      ]},
    ].map(g => ({
      ...g,
      items: g.items.map(it => ({ ...it, count: counts[it.id] }))
    }));
  }

  _sidebar() {
    const groups = this._sidebarGroups();
    return `
      <aside class="tm-sidebar">
        <div class="tm-brand">
          <div class="tm-brand-mark"><ha-icon icon="mdi:checkbox-marked-circle-plus-outline"></ha-icon></div>
          <div class="tm-brand-text">
            TaskMate
            <small>v${PANEL_VERSION}</small>
          </div>
          <a class="tm-brand-wiki" href="https://github.com/tempus2016/taskmate/wiki" target="_blank" rel="noopener noreferrer" title="${this._esc(this._t("panel.wiki_tooltip"))}" aria-label="${this._esc(this._t("panel.wiki_tooltip"))}">
            <ha-icon icon="mdi:book-open-variant"></ha-icon>
          </a>
        </div>
        <nav class="tm-nav">
          ${groups.map(g => `
            <div class="tm-nav-group">
              <div class="tm-nav-head">${this._esc(g.head)}</div>
              ${g.items.map(it => {
                const active = it.id === this._activeTab;
                const urgent = it.id === "activity" && it.count > 0;
                const showCount = it.count != null && it.count > 0;
                return `
                  <button type="button" class="tm-nav-item ${active ? "tm-nav-active" : ""}" data-act="tab" data-tab="${it.id}">
                    <span class="tm-nav-icon"><ha-icon icon="${it.icon}"></ha-icon></span>
                    <span class="tm-nav-label">${this._esc(it.label)}</span>
                    ${showCount ? `<span class="tm-nav-badge ${urgent ? "tm-nav-badge-urgent" : ""}">${it.count}</span>` : ""}
                  </button>
                `;
              }).join("")}
            </div>
          `).join("")}
        </nav>
      </aside>
    `;
  }

  _topbar() {
    const crumb = this._sidebarGroups()
      .flatMap(g => g.items)
      .find(i => i.id === this._activeTab);
    const crumbLabel = crumb ? crumb.label : "";
    const pendingCount = this._state
      ? (this._state.pending_completions || []).length + (this._state.pending_reward_claims || []).length + (this._state.swap_requests || []).length
      : 0;
    return `
      <div class="tm-topbar">
        <button type="button" class="tm-menu-btn" data-act="toggle-ha-menu" aria-label="Menu">
          <ha-icon icon="mdi:menu"></ha-icon>
        </button>
        <div class="tm-crumbs">
          <span class="tm-crumbs-root">TaskMate</span>
          <span class="tm-crumbs-sep">/</span>
          <strong>${this._esc(crumbLabel)}</strong>
        </div>
        ${pendingCount > 0 && this._activeTab !== "activity" ? `
          <button type="button" class="tm-approval-pill" data-act="switch-to-activity" title="${this._t("panel.pending_tooltip", {count: pendingCount})}">
            <span class="tm-approval-dot"></span>
            ${this._t("panel.topbar_pending", {count: pendingCount})}
          </button>
        ` : ""}
      </div>
    `;
  }

  _mobileTabs() {
    const groups = this._sidebarGroups();
    const open = !!this._mobileNavOpen;
    const current = groups.flatMap(g => g.items).find(it => it.id === this._activeTab)
      || { id: this._activeTab, label: "", icon: "mdi:cog-outline" };
    const curUrgent = current.id === "activity" && current.count > 0;
    const curShowCount = current.count != null && current.count > 0;
    return `
      <div class="tm-mobilenav ${open ? "tm-mobilenav-open" : ""}">
        <button type="button" class="tm-mnav-trigger" data-act="toggle-mobile-nav"
                aria-haspopup="true" aria-expanded="${open ? "true" : "false"}">
          <span class="tm-mnav-ico"><ha-icon icon="${this._esc(current.icon)}"></ha-icon></span>
          <span class="tm-mnav-main">
            <span class="tm-mnav-crumb">TaskMate</span>
            <span class="tm-mnav-title">${this._esc(current.label)}</span>
          </span>
          ${curShowCount ? `<span class="tm-mnav-count ${curUrgent ? "tm-mnav-count-urgent" : ""}">${current.count}</span>` : ""}
          <span class="tm-mnav-chev"><ha-icon icon="mdi:chevron-down"></ha-icon></span>
        </button>
        ${open ? `
          <div class="tm-mnav-scrim" data-act="close-mobile-nav"></div>
          <nav class="tm-mnav-sheet">
            ${groups.map(g => `
              <div class="tm-mnav-grp-head">${this._esc(g.head)}</div>
              ${g.items.map(it => {
                const active = it.id === this._activeTab;
                const urgent = it.id === "activity" && it.count > 0;
                const showCount = it.count != null && it.count > 0;
                return `
                  <button type="button" class="tm-mnav-item ${active ? "tm-mnav-item-active" : ""}" data-act="tab" data-tab="${it.id}">
                    <ha-icon class="tm-mnav-item-ico" icon="${this._esc(it.icon)}"></ha-icon>
                    <span class="tm-mnav-item-label">${this._esc(it.label)}</span>
                    ${showCount ? `<span class="tm-mnav-count ${urgent ? "tm-mnav-count-urgent" : ""}">${it.count}</span>` : ""}
                  </button>`;
              }).join("")}
            `).join("")}
          </nav>
        ` : ""}
      </div>
    `;
  }

  _approvalBanner() {
    if (!this._state) return "";
    if (this._activeTab === "activity") return "";
    const completionsP = (this._state.pending_completions || []).length;
    const rewardsP     = (this._state.pending_reward_claims || []).length;
    const swapsP       = (this._state.swap_requests || []).length;
    const total = completionsP + rewardsP + swapsP;
    if (total === 0) return "";
    const parts = [];
    // The count strings already include {count}; pass it bolded so the number
    // renders once (not duplicated by a separate <strong> prefix — issue #573).
    if (completionsP) parts.push(this._t("panel.topbar_chore_count", {count: `<strong>${completionsP}</strong>`}));
    if (rewardsP)     parts.push(this._t("panel.topbar_reward_count", {count: `<strong>${rewardsP}</strong>`}));
    if (swapsP)       parts.push(this._t("panel.topbar_swap_count", {count: `<strong>${swapsP}</strong>`}));
    return `
      <div class="tm-approval-banner">
        <ha-icon icon="mdi:bell-ring-outline"></ha-icon>
        <span>${this._t("panel.topbar_awaiting_approval", {details: parts.join(" · ")})}</span>
        <button type="button" class="tm-btn tm-btn-raised tm-btn-sm" data-act="switch-to-activity">${this._t("panel.btn_review")}</button>
      </div>
    `;
  }

  _renderBody() {
    if (this._loading && !this._state) return `<div class="tm-card tm-loading">${this._t("panel.loading")}</div>`;
    if (this._error) return `
      <div class="tm-card tm-card-error">
        <h2>${this._t("panel.error_failed_to_load")}</h2>
        <p>${this._esc(this._error)}</p>
        <button type="button" class="tm-btn tm-btn-raised" data-act="retry">${this._t("panel.btn_retry")}</button>
      </div>`;
    if (!this._state) return `<div class="tm-card">${this._t("panel.error_no_state")}</div>`;

    switch (this._activeTab) {
      case "children":  return this._renderChildrenTab();
      case "activity":  return this._renderActivityTab();
      case "insights":  return this._renderInsightsTab();
      case "chores":    return this._renderChoresTab();
      case "rewards":   return this._renderRewardsTab();
      case "penalties": return this._renderPenBonTab("penalty");
      case "bonuses":   return this._renderPenBonTab("bonus");
      case "groups":    return this._renderGroupsTab();
      case "quests":    return this._renderQuestsTab();
      case "challenges": return this._renderChallengesTab();
      case "badges":    return this._renderBadgesTab();
      case "templates":     return this._renderTemplatesTab();
      case "notifications": return this._renderNotificationsTab();
      case "audit":         return this._renderAuditTab();
      case "settings":      return this._renderSettingsTab();
      default:          return `<div class="tm-card">${this._t("panel.tab_unknown")}</div>`;
    }
  }

  /**
   * Insight reports (#679). Computed on demand rather than stored — a stale
   * cached report is worse than a slow one — so the tab fetches when opened
   * and when the window changes.
   */
  _renderInsightsTab() {
    const view = this._insightView || "fairness";
    const switcher = `
      <div class="tm-chip-row" style="margin-bottom:14px">
        ${["fairness", "friction", "projection", "health"].map(v => `
          <button type="button" class="tm-chip-btn ${v === view ? "tm-chip-on" : ""}"
                  data-act="insights-view" data-id="${v}">
            ${this._t("panel.insights_view_" + v)}
          </button>
        `).join("")}
      </div>`;
    if (view === "friction") return switcher + this._renderFrictionReport();
    if (view === "projection") return switcher + this._renderProjectionReport();
    if (view === "health") return switcher + this._renderHealthReport();
    return switcher + this._renderFairnessReport();
  }

  /**
   * Friction (#680): which chores aren't working, and what to do about each.
   * Deliberately leads with a suggestion rather than just a diagnosis.
   */
  _renderFrictionReport() {
    const report = this._friction;
    if (!report) {
      this._loadFriction(this._frictionDays || 30);
      return `<div class="tm-card"><div class="tm-empty">${this._t("panel.insights_loading")}</div></div>`;
    }
    const days = this._frictionDays || 30;
    const problems = report.chores.filter(c => c.verdict !== "fine");

    return `
      <div class="tm-card">
        <div class="tm-card-head">
          <h2>${this._t("panel.insights_friction_title")}</h2>
          <div class="tm-chip-row">
            ${[14, 30, 90].map(d => `
              <button type="button" class="tm-chip-btn ${d === days ? "tm-chip-on" : ""}"
                      data-act="friction-window" data-id="${d}">
                ${this._t("panel.insights_last_days", { days: d })}
              </button>
            `).join("")}
          </div>
        </div>
        <span class="tm-field-hint">${this._t("panel.insights_friction_intro", { start: report.start, end: report.end })}</span>

        ${report.chores.length === 0 ? `
          <div class="tm-empty">${this._t("panel.insights_friction_none")}</div>
        ` : `
          <div class="tm-verdict tm-verdict-${report.problem_count ? "warn" : "ok"}">
            ${report.problem_count
              ? this._t("panel.insights_friction_problems", { count: report.problem_count })
              : this._t("panel.insights_friction_healthy")}
          </div>
          <table class="tm-table tm-friction">
            <thead><tr>
              <th>${this._t("panel.insights_col_chore")}</th>
              <th>${this._t("panel.insights_col_done")}</th>
              <th>${this._t("panel.insights_col_rate")}</th>
              <th>${this._t("panel.insights_col_last")}</th>
              <th>${this._t("panel.insights_col_verdict")}</th>
              <th>${this._t("panel.insights_col_suggestion")}</th>
            </tr></thead>
            <tbody>
              ${(problems.length ? problems : report.chores).map(c => `
                <tr>
                  <td><strong>${this._esc(c.name)}</strong>${c.outstanding_misses
                    ? ` <span class="tm-fric-misses" title="${this._t("panel.insights_outstanding_misses")}">⚠ ${c.outstanding_misses}</span>` : ""}</td>
                  <td>${c.completed}${c.expected ? ` / ${c.expected}` : ""}</td>
                  <td>${c.rate === null ? "—" : c.rate + "%"}</td>
                  <td>${c.days_since === null
                    ? this._t("panel.insights_never")
                    : this._t("panel.insights_days_ago", { days: c.days_since })}</td>
                  <td><span class="tm-fric-verdict tm-fric-${c.verdict}">${this._t("panel.insights_verdict_" + c.verdict)}</span></td>
                  <td>${this._t("panel.insights_suggest_" + c.suggestion)}</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
          <span class="tm-field-hint">${this._t("panel.insights_friction_note")}</span>
        `}
      </div>
    `;
  }

  /**
   * Projection (#681): what the week ahead looks like. A ceiling rather than
   * a forecast — shared-pool chores are credited to every eligible child,
   * because the schedule can't know who will actually get there first.
   */
  _renderProjectionReport() {
    const report = this._projection;
    if (!report) {
      this._loadProjection(this._projectionDays || 7);
      return `<div class="tm-card"><div class="tm-empty">${this._t("panel.insights_loading")}</div></div>`;
    }
    const days = this._projectionDays || 7;
    const kids = report.children;
    const maxPoints = Math.max(1, ...kids.map(k => k.points));
    const pointsName = this._state.settings?.points_name || this._t("common.points");

    return `
      <div class="tm-card">
        <div class="tm-card-head">
          <h2>${this._t("panel.insights_projection_title")}</h2>
          <div class="tm-chip-row">
            ${[7, 14, 28].map(d => `
              <button type="button" class="tm-chip-btn ${d === days ? "tm-chip-on" : ""}"
                      data-act="projection-window" data-id="${d}">
                ${this._t("panel.insights_next_days", { days: d })}
              </button>
            `).join("")}
          </div>
        </div>
        <span class="tm-field-hint">${this._t("panel.insights_projection_intro", { start: report.start, end: report.end })}</span>

        <div class="tm-proj-totals">
          ${kids.map(k => `
            <div class="tm-proj-kid">
              <div class="tm-fair-name">${this._esc(k.name)}</div>
              <div class="tm-fair-bar"><i style="width:${Math.round(k.points / maxPoints * 100)}%"></i></div>
              <div class="tm-fair-figs">
                ${this._t("panel.insights_projected_earn", { points: k.points, name: this._esc(pointsName) })}
                · ${k.chores} ${this._t(k.chores === 1 ? "panel.insights_chore" : "panel.insights_chores")}
                · ${this._t("panel.insights_projected_total", { total: k.projected_total })}
              </div>
            </div>
          `).join("")}
        </div>

        ${report.unassigned_points > 0 ? `
          <div class="tm-verdict tm-verdict-warn">
            ${this._t("panel.insights_projection_unassigned", { points: report.unassigned_points })}
          </div>` : ""}

        <table class="tm-table tm-projection">
          <thead><tr>
            <th>${this._t("panel.insights_col_day")}</th>
            ${kids.map(k => `<th>${this._esc(k.name)}</th>`).join("")}
          </tr></thead>
          <tbody>
            ${report.by_day.map(d => `
              <tr>
                <td><strong>${this._t("panel.day_" + d.weekday.slice(0, 3))}</strong> <span class="tm-proj-date">${d.date.slice(5)}</span></td>
                ${kids.map(k => {
                  const cell = d.children.find(c => c.id === k.id) || { points: 0, chores: 0 };
                  return `<td>${cell.chores ? `${cell.points} <span class="tm-proj-count">(${cell.chores})</span>` : "—"}</td>`;
                }).join("")}
              </tr>
            `).join("")}
          </tbody>
        </table>
        <span class="tm-field-hint">${this._t("panel.insights_projection_note")}</span>
      </div>
    `;
  }

  /**
   * Health & diagnostics (#682). Every issue carries a severity, a plain
   * sentence and where to fix it — a diagnostic that only says "3 problems"
   * is one the parent can't act on.
   */
  _renderHealthReport() {
    const report = this._health;
    if (!report) {
      this._loadHealth();
      return `<div class="tm-card"><div class="tm-empty">${this._t("panel.insights_loading")}</div></div>`;
    }
    const kb = (report.storage_bytes / 1024).toFixed(1);
    const counts = report.counts;

    return `
      <div class="tm-card">
        <div class="tm-card-head">
          <h2>${this._t("panel.health_title")}</h2>
          <button type="button" class="tm-btn" data-act="health-refresh">${this._t("panel.health_recheck")}</button>
        </div>

        <div class="tm-verdict tm-verdict-${report.healthy ? "ok" : "warn"}">
          ${report.healthy
            ? this._t("panel.health_all_good")
            : this._t("panel.health_issues", { count: report.issues.filter(i => i.severity !== "info").length })}
        </div>

        ${report.issues.length ? `
          <div class="tm-health-issues">
            ${report.issues.map(i => `
              <div class="tm-health-issue tm-health-${i.severity}">
                <span class="tm-health-sev">${this._t("panel.health_sev_" + i.severity)}</span>
                <span class="tm-health-msg">${this._esc(i.message)}</span>
                <button type="button" class="tm-btn tm-btn-small" data-act="health-goto" data-id="${this._esc(i.where)}">
                  ${this._t("panel.health_goto")}
                </button>
              </div>
            `).join("")}
          </div>
        ` : ""}

        <div class="tm-health-counts">
          ${Object.entries(counts).map(([key, value]) => `
            <div class="tm-health-stat">
              <div class="tm-health-num">${value}</div>
              <div class="tm-health-label">${this._t("panel.health_count_" + key)}</div>
            </div>
          `).join("")}
          <div class="tm-health-stat">
            <div class="tm-health-num">${kb} KB</div>
            <div class="tm-health-label">${this._t("panel.health_count_storage")}</div>
          </div>
        </div>
      </div>
    `;
  }

  async _loadHealth() {
    if (this._healthLoading) return;
    this._healthLoading = true;
    const { ok, res, err } = await this._callWS({ type: "taskmate/reports/health" });
    this._healthLoading = false;
    if (!ok) { this._showToast("err", err); return; }
    this._health = res;
    this._render();
  }

  async _loadProjection(days) {
    if (this._projectionLoading) return;
    this._projectionLoading = true;
    const { ok, res, err } = await this._callWS({ type: "taskmate/reports/projection", days });
    this._projectionLoading = false;
    if (!ok) { this._showToast("err", err); return; }
    this._projection = res;
    this._render();
  }

  async _loadFriction(days) {
    if (this._frictionLoading) return;
    this._frictionLoading = true;
    const { ok, res, err } = await this._callWS({ type: "taskmate/reports/friction", days });
    this._frictionLoading = false;
    if (!ok) { this._showToast("err", err); return; }
    this._friction = res;
    this._render();
  }

  _renderFairnessReport() {
    const days = this._insightDays || 7;
    const report = this._fairness;
    if (!report) {
      this._loadFairness(days);
      return `<div class="tm-card"><div class="tm-empty">${this._t("panel.insights_loading")}</div></div>`;
    }

    const windows = [7, 14, 30];
    const maxShare = Math.max(1, ...report.children.map(c => c.share_completions));

    return `
      <div class="tm-card">
        <div class="tm-card-head">
          <h2>${this._t("panel.insights_fairness_title")}</h2>
          <div class="tm-chip-row">
            ${windows.map(d => `
              <button type="button" class="tm-chip-btn ${d === days ? "tm-chip-on" : ""}"
                      data-act="insights-window" data-id="${d}">
                ${this._t("panel.insights_last_days", { days: d })}
              </button>
            `).join("")}
          </div>
        </div>
        <span class="tm-field-hint">${this._t("panel.insights_fairness_intro", {
          start: report.start, end: report.end, fair: report.fair_share,
        })}</span>

        ${report.total_completions === 0 ? `
          <div class="tm-empty">${this._t("panel.insights_no_data")}</div>
        ` : `
          <div class="tm-verdict tm-verdict-${report.balanced ? "ok" : "warn"}">
            ${report.balanced
              ? this._t("panel.insights_balanced")
              : this._t("panel.insights_unbalanced")}
          </div>
          <div class="tm-fairness">
            ${report.children.map(c => `
              <div class="tm-fair-row">
                <div class="tm-fair-name">${this._esc(c.name)}</div>
                <div class="tm-fair-bar">
                  <i class="tm-fair-${c.status}" style="width:${Math.round(c.share_completions / maxShare * 100)}%"></i>
                  <span class="tm-fair-target" style="left:${Math.round(report.fair_share / maxShare * 100)}%"></span>
                </div>
                <div class="tm-fair-figs">
                  <strong>${c.completions}</strong> ${this._t(c.completions === 1 ? "panel.insights_chore" : "panel.insights_chores")}
                  · ${c.points} ${this._esc(this._state.settings?.points_name || this._t("common.points"))}
                  · ${c.share_completions}%
                </div>
                <div class="tm-fair-status tm-fair-${c.status}">${this._t("panel.insights_status_" + c.status)}</div>
              </div>
            `).join("")}
          </div>
          <span class="tm-field-hint">${this._t("panel.insights_fairness_note", { tolerance: report.tolerance })}</span>
        `}
      </div>
    `;
  }

  async _loadFairness(days) {
    if (this._fairnessLoading) return;
    this._fairnessLoading = true;
    const { ok, res, err } = await this._callWS({ type: "taskmate/reports/fairness", days });
    this._fairnessLoading = false;
    if (!ok) { this._showToast("err", err); return; }
    this._fairness = res;
    this._render();
  }

  _searchBox(placeholder) {
    return `
      <div class="tm-search-wrap">
        <ha-icon icon="mdi:magnify" class="tm-search-icon"></ha-icon>
        <input class="tm-search" placeholder="${this._esc(placeholder)}" data-filter="true" value="${this._esc(this._filter)}">
        ${this._filter ? `<button type="button" class="tm-search-clear" data-act="clear-filter" title="${this._t("panel.tooltip_clear")}">✕</button>` : ""}
      </div>
    `;
  }

  _filterByName(items) {
    if (!this._filter) return items;
    const q = this._filter.toLowerCase();
    return items.filter(it => (it.name || "").toLowerCase().includes(q));
  }

  // -- Children tab ------------------------------------------------------
  _renderChildrenTab() {
    const all = this._state.children || [];
    const children = this._filterByName(all);
    const pointsName = this._state.settings.points_name || this._t("common.points");
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.child_title")} <span class="tm-toolbar-count">${all.length}</span></h2>
        ${all.length > 0 ? this._searchBox(this._t("panel.search_children")) : ""}
        ${all.length >= 2 ? `<button type="button" class="tm-btn" data-act="gift-points">${this._t("panel.gift_btn")}</button>` : ""}
        <button type="button" class="tm-btn tm-btn-raised" data-act="add-child">${this._t("panel.btn_add_child")}</button>
      </div>
      ${all.length === 0 ? this._emptyState("👨‍👩‍👧‍👦", this._t("panel.empty_children_title"), this._t("panel.empty_children_copy"), "add-child", this._t("panel.btn_add_child")) :
        children.length === 0 ? `<div class="tm-card tm-empty"><p>${this._t("panel.empty_no_match", {kind: this._t("panel.tab_children").toLowerCase(), filter: this._esc(this._filter)})}</p></div>` : `
        <div class="tm-grid">
          ${children.map(c => this._renderChildCard(c, pointsName)).join("")}
          <button type="button" class="tm-add-tile" data-act="add-child"><span class="tm-add-plus">＋</span>${this._t("panel.child_add_tile")}</button>
        </div>
      `}
    `;
  }

  _renderChildCard(child, pointsName) {
    return `
      <article class="tm-card tm-child-card">
        <div class="tm-child-head">
          <div class="tm-avatar">${this._mdi(child.avatar)}</div>
          <div class="tm-child-name">
            <h3>${this._esc(child.name || this._t("panel.child_unnamed"))}</h3>
            ${this._idBadge(child.id)}
            <div class="tm-meta">${child.availability_entity ? `<code title="${this._esc(child.availability_entity)}">${this._esc(child.availability_entity)}</code>${child.availability_inverted ? ` <em>${this._t("panel.child_inverted")}</em>` : ""}` : `${this._t("panel.child_no_availability")}`}${child.unavailability_entity ? ` · ${this._t("panel.child_busy_label")} <code title="${this._esc(child.unavailability_entity)}">${this._esc(child.unavailability_entity)}</code>` : ""}</div>
          </div>
        </div>
        <div class="tm-stats-row" style="grid-template-columns: 1fr 1fr;">
          <div class="tm-stat tm-stat-highlight"><div class="tm-stat-value">${this._fmtNum(child.points || 0)}</div><div class="tm-stat-label">${this._esc(pointsName)}</div></div>
          <div class="tm-stat"><div class="tm-stat-value">${this._fmtNum(child.level || 1)}</div><div class="tm-stat-label">${this._t("panel.child_stat_level")}</div></div>
          <div class="tm-stat"><div class="tm-stat-value">${this._fmtNum(child.total_points_earned || 0)}</div><div class="tm-stat-label">${this._t("panel.child_stat_earned")}</div></div>
          <div class="tm-stat"><div class="tm-stat-value">${this._fmtNum(child.total_chores_completed || 0)}</div><div class="tm-stat-label">${this._t("panel.child_stat_done")}</div></div>
        </div>
        ${this._renderAdjustStrip(child, pointsName)}
        <div class="tm-card-foot">
          <button type="button" class="tm-btn tm-btn-sm" data-act="edit-child" data-id="${this._esc(child.id)}">${this._t("panel.btn_edit")}</button>
          <button type="button" class="tm-btn tm-btn-sm" data-act="reorder-chores-for-child" data-id="${this._esc(child.id)}" title="${this._t("panel.chore_order_title")}">⇅ ${this._t("panel.chore_order_title")}</button>
          <button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="delete-child" data-id="${this._esc(child.id)}">${this._t("panel.btn_delete")}</button>
        </div>
      </article>
    `;
  }

  // Manual point adjustment (#746). Minus amounts descend and plus amounts
  // ascend so the two smallest meet in the middle and the row reads outward
  // from zero. The two `-set` spans are load-bearing: with all seven buttons as
  // direct flex children, a narrow card wraps only the last one and orphans ⋯
  // on a row of its own. Grouping makes each side wrap as a whole unit.
  _renderAdjustStrip(child, pointsName) {
    const amounts = this._quickPointAmounts();
    const id = this._esc(child.id);
    const name = child.name || this._t("panel.child_unnamed");
    const btn = (delta) => {
      const amount = Math.abs(delta);
      const label = this._t(delta < 0 ? "panel.adjust_remove_title" : "panel.adjust_add_title",
                            { amount, points: pointsName, child: name });
      return `<button type="button" class="tm-btn tm-btn-sm ${delta < 0 ? "tm-btn-danger" : "tm-btn-raised"}"
                data-act="adjust-points" data-id="${id}" data-delta="${delta}"
                title="${this._esc(label)}" aria-label="${this._esc(label)}">${delta < 0 ? "−" : "+"}${amount}</button>`;
    };
    const customLabel = this._t("panel.adjust_custom_title", { child: name });
    return `
      <div class="tm-points-adjust">
        <span class="tm-points-adjust-set">${amounts.slice().reverse().map(a => btn(-a)).join("")}</span>
        <span class="tm-points-adjust-gap"></span>
        <span class="tm-points-adjust-set">
          ${amounts.map(a => btn(a)).join("")}
          <button type="button" class="tm-btn tm-btn-sm" data-act="adjust-points-custom" data-id="${id}"
                  title="${this._esc(customLabel)}" aria-label="${this._esc(customLabel)}">⋯</button>
        </span>
      </div>`;
  }

  // -- Activity tab ------------------------------------------------------
  _renderActivityTab() {
    const pendingCompletions = this._state.pending_completions || [];
    const pendingClaims      = this._state.pending_reward_claims || [];
    const pendingSwaps       = this._state.swap_requests || [];
    const transactions       = this._state.points_transactions || [];
    const completions        = this._state.completions || [];
    const claims             = this._state.reward_claims || [];

    const childById = Object.fromEntries((this._state.children || []).map(c => [c.id, c]));
    const choreById = Object.fromEntries((this._state.chores || []).map(c => [c.id, c]));
    const rewardById = Object.fromEntries((this._state.rewards || []).map(r => [r.id, r]));

    // Recent activity feed: merge approved completions + claims + transactions
    const events = [];
    completions.filter(c => c.approved).forEach(c => events.push({
      ts: c.approved_at || c.completed_at, kind: "completion",
      child: c.child_id === "__parent__" ? "Parent" : ((childById[c.child_id] || {}).name || "?"),
      label: `${this._t("panel.activity_completed_chore", {name: (choreById[c.chore_id] || {}).name || this._t("panel.activity_deleted_chore")})}`,
      points: c.points_awarded,
    }));
    claims.filter(c => c.approved).forEach(c => events.push({
      ts: c.approved_at || c.claimed_at, kind: "claim",
      child: (childById[c.child_id] || {}).name || "?",
      label: `${this._t("panel.activity_claimed_reward", {name: (rewardById[c.reward_id] || {}).name || this._t("panel.activity_deleted_reward")})}`,
      points: -((rewardById[c.reward_id] || {}).cost || 0),
    }));
    transactions.forEach(t => events.push({
      ts: t.created_at, kind: "manual",
      child: (childById[t.child_id] || {}).name || "?",
      label: this._translateReason(t.reason) || (t.points >= 0 ? this._t("panel.activity_manual_addition") : this._t("panel.activity_manual_deduction")),
      points: t.points,
    }));
    events.sort((a, b) => (b.ts || "").localeCompare(a.ts || ""));
    const recent = events.slice(0, 30);

    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.activity_title")}</h2>
      </div>

      <!-- Pending approvals -->
      <div class="tm-card">
        <h3 class="tm-section-title">${this._t("panel.activity_pending_approvals")}
          ${(pendingCompletions.length + pendingClaims.length + pendingSwaps.length) > 0
            ? `<span class="tm-pill tm-pill-warn">${pendingCompletions.length + pendingClaims.length + pendingSwaps.length}</span>`
            : `<span class="tm-pill tm-pill-success">${this._t("panel.activity_pending_all_clear")}</span>`}
          ${pendingCompletions.length > 0
            ? `<button type="button" class="tm-btn tm-btn-raised tm-btn-sm" data-act="approve-all-chores" style="margin-left:auto"${this._approvingAll ? " disabled" : ""}>${this._approvingAll
                ? `<span class="tm-btn-spinner"></span>${this._t("panel.activity_approve_all_busy")}`
                : this._t("panel.activity_approve_all")}</button>`
            : ""}
        </h3>
        ${pendingCompletions.length === 0 && pendingClaims.length === 0 && pendingSwaps.length === 0 ? `
          <p class="tm-meta">${this._t("panel.activity_no_items")}</p>
        ` : `
          <div class="tm-approval-list">
            ${pendingCompletions.map(c => {
              const chore = choreById[c.chore_id];
              const child = childById[c.child_id];
              let choreName = (chore && chore.name) || this._t("panel.activity_deleted_chore");
              let chorePoints = chore ? chore.points : 0;
              if (c.bonus_subtask_id && chore) {
                const sub = (chore.bonus_subtasks || []).find(b => b.id === c.bonus_subtask_id);
                if (sub) { choreName = `${chore.name} › ${sub.name}`; chorePoints = sub.points; }
              } else if (chore && chore.task_type === "timed" && (c.timed_duration_seconds || 0) > 0) {
                // Timed chores accrue points by elapsed-time rate, so the actual
                // earned points can be lower than chore.points. Mirror sensor.py.
                const rateSeconds = (chore.timed_rate_minutes || 0) * 60;
                if (rateSeconds > 0) {
                  chorePoints = Math.floor(c.timed_duration_seconds / rateSeconds) * (chore.timed_rate_points || 0);
                }
              }
              const photoCap = [choreName, (child && child.name) || "", c.completed_at ? new Date(c.completed_at).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""].filter(Boolean).join(" · ");
              return `
                <div class="tm-approval-item">
                  <div class="tm-approval-icon"><ha-icon icon="${c.bonus_subtask_id ? 'mdi:star-plus' : 'mdi:checkbox-marked-circle-outline'}"></ha-icon></div>
                  ${this._safePhotoUrl(c.photo_url) ? `<a class="tm-approval-photo" href="${this._esc(this._safePhotoUrl(c.photo_url))}" target="_blank" rel="noopener" data-act="view-photo" data-photo="${this._esc(this._safePhotoUrl(c.photo_url))}" data-cap="${this._esc(photoCap)}" title="${this._t("panel.activity_view_photo")}"><img src="${this._esc(this._safePhotoUrl(c.photo_url))}" alt="" loading="lazy"></a>` : ""}
                  <div class="tm-approval-body">
                    <div class="tm-approval-line">${this._t("panel.activity_completed_text", {child: this._esc((child && child.name) || "?"), chore: this._esc(choreName)})}</div>
                    <div class="tm-meta">${this._timeAgo(c.completed_at)} · ${chorePoints} ${this._t("panel.activity_points")}</div>
                  </div>
                  <div class="tm-approval-actions">
                    <button type="button" class="tm-btn tm-btn-sm" data-act="reject-chore" data-id="${this._esc(c.id)}">${this._t("panel.activity_reject")}</button>
                    <button type="button" class="tm-btn tm-btn-raised tm-btn-sm" data-act="approve-chore" data-id="${this._esc(c.id)}">${this._t("panel.activity_approve")}</button>
                  </div>
                </div>
              `;
            }).join("")}
            ${pendingClaims.map(c => {
              const reward = rewardById[c.reward_id];
              const child = childById[c.child_id];
              return `
                <div class="tm-approval-item">
                  <div class="tm-approval-icon"><ha-icon icon="mdi:gift-outline"></ha-icon></div>
                  <div class="tm-approval-body">
                    <div class="tm-approval-line">${this._t("panel.activity_claimed_text", {child: this._esc((child && child.name) || "?"), reward: this._esc((reward && reward.name) || this._t("panel.activity_deleted_reward"))})}</div>
                    <div class="tm-meta">${this._timeAgo(c.claimed_at)}${reward ? ` · ${reward.cost} ${this._t("panel.activity_points")}` : ""}</div>
                  </div>
                  <div class="tm-approval-actions">
                    <button type="button" class="tm-btn tm-btn-sm" data-act="reject-reward" data-id="${this._esc(c.id)}">${this._t("panel.activity_reject")}</button>
                    <button type="button" class="tm-btn tm-btn-raised tm-btn-sm" data-act="approve-reward" data-id="${this._esc(c.id)}">${this._t("panel.activity_approve")}</button>
                  </div>
                </div>
              `;
            }).join("")}
            ${pendingSwaps.map(sw => {
              const chore = choreById[sw.chore_id];
              const req = childById[sw.requester_id];
              const from = childById[sw.from_child_id];
              return `
                <div class="tm-approval-item">
                  <div class="tm-approval-icon"><ha-icon icon="mdi:swap-horizontal"></ha-icon></div>
                  <div class="tm-approval-body">
                    <div class="tm-approval-line">${this._t("panel.swap_request_text", {child: this._esc((req && req.name) || "?"), chore: this._esc((chore && chore.name) || "?")})}</div>
                    <div class="tm-meta">${from ? this._t("panel.swap_from", {name: this._esc(from.name)}) + " · " : ""}${this._timeAgo(sw.created_at)}</div>
                  </div>
                  <div class="tm-approval-actions">
                    <button type="button" class="tm-btn tm-btn-sm" data-act="reject-swap" data-id="${this._esc(sw.id)}">${this._t("panel.activity_reject")}</button>
                    <button type="button" class="tm-btn tm-btn-raised tm-btn-sm" data-act="approve-swap" data-id="${this._esc(sw.id)}">${this._t("panel.activity_approve")}</button>
                  </div>
                </div>
              `;
            }).join("")}
          </div>
        `}
      </div>

      <!-- Recent activity feed -->
      <div class="tm-card">
        <h3 class="tm-section-title">${this._t("panel.activity_recent")}</h3>
        ${recent.length === 0 ? `<p class="tm-meta">${this._t("panel.activity_no_recent")}</p>` : `
          <div class="tm-timeline">
            ${recent.map(ev => `
              <div class="tm-timeline-row">
                <div class="tm-timeline-time">${this._esc(this._timeAgo(ev.ts))}</div>
                <div class="tm-timeline-icon tm-timeline-${ev.kind}"><ha-icon icon="${ev.kind === 'completion' ? 'mdi:check-circle' : ev.kind === 'claim' ? 'mdi:gift' : ev.points >= 0 ? 'mdi:plus-circle' : 'mdi:minus-circle'}"></ha-icon></div>
                <div class="tm-timeline-body">
                  <div><strong>${this._esc(ev.child)}</strong> · ${this._esc(ev.label)}</div>
                </div>
                <div class="tm-timeline-points ${ev.points >= 0 ? 'tm-pos' : 'tm-neg'} tm-numeric">${ev.points >= 0 ? '+' : ''}${ev.points || 0}</div>
              </div>
            `).join("")}
          </div>
        `}
      </div>

      <!-- Audit log -->
      <div class="tm-card">
        <h3 class="tm-section-title">${this._t("panel.activity_audit_log")}
          <span class="tm-pill">${this._t(transactions.length === 1 ? "panel.activity_transaction_count" : "panel.activity_transaction_count_plural", {count: transactions.length})}</span>
        </h3>
        ${transactions.length === 0 ? `<p class="tm-meta">${this._t("panel.activity_no_points_transactions")}</p>` : `
          <div class="tm-table-wrap">
            <table class="tm-table">
              <thead><tr><th>${this._t("panel.activity_table_when")}</th><th>${this._t("panel.activity_table_child")}</th><th>${this._t("panel.activity_table_points")}</th><th>${this._t("panel.activity_table_reason")}</th><th></th></tr></thead>
              <tbody>
                ${[...transactions].reverse().map(t => {
                  const child = childById[t.child_id];
                  const undoable = this._txnReversible(t.reason);
                  return `
                    <tr class="tm-row">
                      <td class="tm-meta">${this._esc(this._timeAgo(t.created_at))}</td>
                      <td>${this._esc((child && child.name) || "?")}</td>
                      <td><strong class="tm-numeric ${t.points >= 0 ? 'tm-pos' : 'tm-neg'}">${t.points >= 0 ? '+' : ''}${t.points}</strong></td>
                      <td>${this._esc(this._translateReason(t.reason) || "—")}</td>
                      <td>${undoable ? `<button type="button" class="tm-icon-btn" data-act="undo-tx" data-id="${this._esc(t.id)}" title="${this._esc(this._t("panel.activity_undo"))}"><ha-icon icon="mdi:undo-variant"></ha-icon></button>` : ""}</td>
                    </tr>
                  `;
                }).join("")}
              </tbody>
            </table>
          </div>
        `}
      </div>
    `;
  }

  // -- Chores tab --------------------------------------------------------
  _sortChoresByDisplayOrder(chores) {
    const order = this._state.chore_display_order || [];
    if (!order.length) return chores;
    const idx = Object.fromEntries(order.map((id, i) => [id, i]));
    return [...chores].sort((a, b) => {
      const ai = idx[a.id] ?? Number.MAX_SAFE_INTEGER;
      const bi = idx[b.id] ?? Number.MAX_SAFE_INTEGER;
      return ai - bi;
    });
  }

  _renderChoresTab() {
    const raw = this._state.chores || [];
    const all = this._sortChoresByDisplayOrder(raw);
    const chores = this._filterByName(all);
    const childById = Object.fromEntries((this._state.children || []).map(c => [c.id, c]));
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.chore_title")} <span class="tm-toolbar-count">${all.length}</span></h2>
        ${all.length > 0 ? this._searchBox(this._t("panel.search_chores")) : ""}
        <button type="button" class="tm-btn" data-act="reorder-chores-global">${this._t("panel.btn_reorder_chores")}</button>
        <button type="button" class="tm-btn" data-act="bulk-add-chore">${this._t("panel.btn_bulk_add")}</button>
        <button type="button" class="tm-btn" data-act="tpl-add-from">${this._t("panel.btn_from_template")}</button>
        <button type="button" class="tm-btn" data-act="tpl-save-from">${this._t("panel.btn_save_as_template")}</button>
        ${all.length > 0 ? `<button type="button" class="tm-btn ${this._bulkMode ? "tm-btn-on" : ""}" data-act="bulk-toggle">${this._bulkMode ? this._t("panel.bulk_done") : this._t("panel.bulk_select")}</button>` : ""}
        <button type="button" class="tm-btn tm-btn-raised" data-act="add-chore">${this._t("panel.btn_add_chore")}</button>
      </div>
      ${this._bulkMode && all.length > 0 ? `
        <div class="tm-bulk-bar">
          <span class="tm-bulk-count">${this._t("panel.bulk_selected", { count: this._bulkSel.size })}</span>
          <button type="button" class="tm-btn" data-act="bulk-enable" ${this._bulkSel.size ? "" : "disabled"}>${this._t("panel.bulk_enable")}</button>
          <button type="button" class="tm-btn" data-act="bulk-disable" ${this._bulkSel.size ? "" : "disabled"}>${this._t("panel.bulk_disable")}</button>
          <button type="button" class="tm-btn" data-act="bulk-delete" ${this._bulkSel.size ? "" : "disabled"}>${this._t("panel.bulk_delete")}</button>
          <select class="tm-select" data-role="bulk-reassign">
            <option value="">${this._t("panel.bulk_reassign_to")}</option>
            <option value="__all__">${this._t("panel.common_all_children")}</option>
            ${(this._state.children || []).map(c => `<option value="${this._esc(c.id)}">${this._esc(c.name)}</option>`).join("")}
          </select>
          <button type="button" class="tm-btn" data-act="bulk-reassign" ${this._bulkSel.size ? "" : "disabled"}>${this._t("panel.bulk_apply")}</button>
        </div>` : ""}
      ${all.length === 0 ? this._emptyState("📋", this._t("panel.empty_chores_title"), this._t("panel.empty_chores_copy"), "add-chore", this._t("panel.btn_add_chore")) :
        chores.length === 0 ? `<div class="tm-card tm-empty"><p>${this._t("panel.empty_no_match", {kind: this._t("panel.tab_chores").toLowerCase(), filter: this._esc(this._filter)})}</p></div>` : `
        <div class="tm-table-wrap">
          <table class="tm-table">
            <thead><tr>
              ${this._bulkMode ? "<th></th>" : ""}<th class="${this._bulkMode ? "" : "tm-col-sticky"}">${this._t("panel.chore_table_name")}</th><th>${this._t("panel.chore_table_points")}</th><th>${this._t("panel.chore_table_period")}</th><th>${this._t("panel.chore_table_assigned")}</th><th>${this._t("panel.chore_table_current")}</th><th>${this._t("panel.chore_table_schedule")}</th><th>${this._t("panel.chore_table_approval")}</th>
            </tr></thead>
            <tbody>
              ${chores.map(c => this._renderChoreRow(c, childById)).join("")}
            </tbody>
          </table>
        </div>
        <p class="tm-meta tm-table-hint">${this._t("panel.chore_tip_rename")}</p>
      `}
    `;
  }

  _renderChoreRow(c, childById) {
    const renaming = this._inlineRename && this._inlineRename.kind === "chore" && this._inlineRename.id === c.id;
    const isRotation = ["alternating", "random", "balanced"].includes(c.assignment_mode);
    const curChild = c.assignment_current_child_id || "";
    const assignedNames = c.assignment_mode === "unassigned"
      ? `<span class="tm-text-muted">${this._t("panel.assign_unassigned_short")}</span>`
      : (c.assigned_to || []).length === 0
      ? `<span class="tm-text-muted">${this._t("panel.common_all_children")}</span>`
      : this._esc((c.assigned_to || []).map(id => (childById[id] && childById[id].name) || "?").join(", "));
    const currentName = isRotation && curChild && childById[curChild]
      ? `<strong>${this._esc(childById[curChild].name)}</strong>`
      : isRotation ? `<span class="tm-text-muted">—</span>` : "";
    const schedLabel = c.schedule_mode === "recurring"
      ? this._labelOf(RECURRENCES, c.recurrence) + (c.recurrence_day && c.recurrence_day !== "any_day" ? ` · ${this._labelOf(DAYS, c.recurrence_day)}` : "")
      : c.schedule_mode === "one_shot"
      ? this._t("panel.common_one_shot")
      : ((c.due_days || []).length === 0 ? this._t("panel.common_daily") : (c.due_days || []).map(d => this._labelOf(DAYS, d)).join(" · "));
    const schedClass = c.schedule_mode === "recurring" ? "tm-pill-accent" : c.schedule_mode === "one_shot" ? "tm-pill-warn" : "tm-pill-success";
    const modeBadge = c.assignment_mode && c.assignment_mode !== "everyone"
      ? `<span class="tm-pill tm-pill-${c.assignment_mode}">${this._t(`panel.assign_${c.assignment_mode}_short`)}</span>` : "";
    const nameCell = renaming
      ? `<input class="tm-inline-input" type="text" data-field="_inlineRename" value="${this._esc(this._inlineRename.value)}" autofocus>
         <button type="button" class="tm-icon-btn" data-act="rename-chore-commit" title="${this._t("panel.tooltip_save")}">✓</button>
         <button type="button" class="tm-icon-btn" data-act="rename-chore-cancel" title="${this._t("panel.tooltip_cancel")}">✕</button>`
      : `<strong data-rename="chore" data-id="${this._esc(c.id)}" title="${this._t("panel.tooltip_rename")}">${this._esc(c.name)}</strong>${c.enabled === false ? ` <span class="tm-pill">${this._t("panel.common_disabled")}</span>` : ""}${this._idBadge(c.id)}`;
    return `
      <tr class="tm-row ${c.enabled === false ? "tm-row-disabled" : ""}">
        ${this._bulkMode ? `<td><input type="checkbox" data-act="bulk-toggle-row" data-id="${this._esc(c.id)}" ${this._bulkSel.has(c.id) ? "checked" : ""}></td>` : ""}
        <td class="${this._bulkMode ? "" : "tm-col-sticky"}"><div class="tm-name-cell">
          <span class="tm-name-main">${nameCell}</span>
          ${renaming ? "" : `<span class="tm-name-actions">
            <button type="button" class="tm-icon-btn" data-act="edit-chore" data-id="${this._esc(c.id)}" title="${this._t("panel.btn_edit")}">✏</button>
            <button type="button" class="tm-icon-btn" data-act="toggle-row-menu" data-id="${this._esc(c.id)}" title="${this._t("panel.row_menu_more")}" aria-haspopup="menu" aria-label="${this._t("panel.row_menu_more")}">⋮</button>
          </span>`}
        </div></td>
        <td><strong class="tm-numeric">${c.task_type === "timed" ? `${c.timed_rate_points || 0}/${c.timed_rate_minutes || 1} min` : c.points}</strong></td>
        <td><span class="tm-pill">${this._esc(this._timeCategoryLabel(c.time_category))}</span></td>
        <td>${assignedNames} ${modeBadge}</td>
        <td>${currentName}</td>
        <td><span class="tm-pill ${schedClass} tm-pill-dot">${this._esc(schedLabel)}</span></td>
        <td>${c.requires_approval ? `<span class='tm-yes'>${this._t("panel.common_yes")}</span>` : `<span class='tm-no'>${this._t("panel.common_no")}</span>`}</td>
      </tr>
    `;
  }

  // -- Rewards tab -------------------------------------------------------
  _renderRewardsTab() {
    const all = this._state.rewards || [];
    const rewards = this._filterByName(all);
    const pointsName = this._state.settings.points_name || this._t("common.points");
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.reward_title")} <span class="tm-toolbar-count">${all.length}</span></h2>
        ${all.length > 0 ? this._searchBox(this._t("panel.search_rewards")) : ""}
        <button type="button" class="tm-btn tm-btn-raised" data-act="add-reward">${this._t("panel.btn_add_reward")}</button>
      </div>
      ${all.length === 0 ? this._emptyState("🎁", this._t("panel.empty_rewards_title"), this._t("panel.empty_rewards_copy", {points_name: pointsName.toLowerCase()}), "add-reward", this._t("panel.btn_add_reward")) :
        rewards.length === 0 ? `<div class="tm-card tm-empty"><p>${this._t("panel.empty_no_match", {kind: this._t("panel.tab_rewards").toLowerCase(), filter: this._esc(this._filter)})}</p></div>` : `
        <div class="tm-grid">
          ${rewards.map(r => this._renderRewardCard(r, pointsName)).join("")}
          <button type="button" class="tm-add-tile" data-act="add-reward"><span class="tm-add-plus">＋</span>${this._t("panel.reward_add_tile")}</button>
        </div>
      `}
    `;
  }

  _renderRewardCard(r, pointsName) {
    const totalPooled = (this._state.pool_allocations || []).filter(a => a.reward_id === r.id).reduce((s, a) => s + (a.allocated_points || 0), 0);
    const showProgress = (r.pool_enabled || r.is_jackpot) && r.cost > 0;
    const pct = showProgress ? Math.min(100, Math.round(totalPooled / r.cost * 100)) : 0;
    return `
      <article class="tm-card tm-reward-card">
        <div class="tm-child-head">
          <div class="tm-avatar tm-avatar-reward">${this._mdi(r.icon || "mdi:gift")}</div>
          <div class="tm-child-name">
            <h3>${this._esc(r.name)} ${r.is_jackpot ? `<span class="tm-pill tm-pill-jackpot">🏆 ${this._t("panel.reward_badge_jackpot")}</span>` : ""} ${r.pool_enabled ? `<span class="tm-pill tm-pill-pool">${this._t("panel.reward_badge_pool")}</span>` : ""}</h3>
            ${this._idBadge(r.id)}
            <div class="tm-meta">${this._esc(r.description || "")}</div>
          </div>
        </div>
        <div class="tm-stats-row" style="grid-template-columns: 1fr 1fr ${r.expires_at ? "1fr" : ""};">
          <div class="tm-stat"><div class="tm-stat-value tm-numeric tm-cost">${this._fmtNum(r.cost)}</div><div class="tm-stat-label">${this._t("panel.reward_stat_cost", {points_name: pointsName})}</div></div>
          ${r.quantity != null ? `<div class="tm-stat"><div class="tm-stat-value tm-numeric">${r.quantity}</div><div class="tm-stat-label">${this._t("panel.reward_stat_remaining")}</div></div>` : `<div class="tm-stat"><div class="tm-stat-value">∞</div><div class="tm-stat-label">${this._t("panel.reward_stat_unlimited")}</div></div>`}
          ${r.expires_at ? `<div class="tm-stat"><div class="tm-stat-value" style="font-size: 14px;">${this._esc(r.expires_at)}</div><div class="tm-stat-label">${this._t("panel.reward_stat_expires")}</div></div>` : ""}
        </div>
        ${showProgress ? `
          <div class="tm-progress"><span style="width:${pct}%"></span></div>
          <div class="tm-progress-text"><span><strong>${this._fmtNum(totalPooled)}</strong> / ${this._fmtNum(r.cost)}</span><span>${pct}%</span></div>
        ` : ""}
        <div class="tm-card-foot">
          <button type="button" class="tm-btn tm-btn-sm" data-act="edit-reward" data-id="${this._esc(r.id)}">${this._t("panel.btn_edit")}</button>
          <button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="delete-reward" data-id="${this._esc(r.id)}">${this._t("panel.btn_delete")}</button>
        </div>
      </article>
    `;
  }

  // -- Quests tab --------------------------------------------------------
  _renderQuestsTab() {
    const all = this._state.quests || [];
    const quests = this._filterByName(all);
    const pointsName = this._state.settings.points_name || this._t("common.points");
    const haveChores = (this._state.chores || []).length > 0;
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.quest_title")} <span class="tm-toolbar-count">${all.length}</span></h2>
        ${all.length > 0 ? this._searchBox(this._t("panel.search_quests")) : ""}
        ${haveChores ? `<button type="button" class="tm-btn tm-btn-raised" data-act="add-quest">${this._t("panel.btn_add_quest")}</button>` : ""}
      </div>
      ${!haveChores ? `<div class="tm-card tm-empty"><p>${this._t("panel.quest_need_chores")}</p></div>` :
        all.length === 0 ? this._emptyState("🗺️", this._t("panel.empty_quests_title"), this._t("panel.empty_quests_copy"), "add-quest", this._t("panel.btn_add_quest")) :
        quests.length === 0 ? `<div class="tm-card tm-empty"><p>${this._t("panel.empty_no_match", {kind: this._t("panel.tab_quests").toLowerCase(), filter: this._esc(this._filter)})}</p></div>` : `
        <div class="tm-grid">
          ${quests.map(q => this._renderQuestCard(q, pointsName)).join("")}
          <button type="button" class="tm-add-tile" data-act="add-quest"><span class="tm-add-plus">＋</span>${this._t("panel.quest_add_tile")}</button>
        </div>
      `}
    `;
  }

  _renderQuestCard(q, pointsName) {
    const choreById = {};
    (this._state.chores || []).forEach(c => { choreById[c.id] = c; });
    const steps = q.steps || [];
    const children = this._state.children || [];
    const assigned = (q.assigned_to && q.assigned_to.length)
      ? children.filter(c => q.assigned_to.includes(c.id)).map(c => c.name)
      : [this._t("panel.quest_all_children")];
    const stepNames = steps.map((sid) => {
      const name = choreById[sid] ? choreById[sid].name : this._t("panel.quest_missing_chore");
      return `<li>${this._esc(name)}</li>`;
    }).join("");
    return `
      <article class="tm-card tm-quest-card${q.active ? "" : " tm-quest-inactive"}">
        <div class="tm-child-head">
          <div class="tm-avatar">${this._mdi(q.icon || "mdi:map-marker-path")}</div>
          <div class="tm-child-name">
            <h3>${this._esc(q.name)}
              ${q.repeatable ? `<span class="tm-pill">${this._t("panel.quest_badge_repeatable")}</span>` : ""}
              ${q.active ? "" : `<span class="tm-pill tm-pill-muted">${this._t("panel.quest_badge_inactive")}</span>`}
            </h3>
            ${this._idBadge(q.id)}
            <div class="tm-meta">${this._esc(q.description || "")}</div>
          </div>
        </div>
        <div class="tm-stats-row" style="grid-template-columns: 1fr 1fr;">
          <div class="tm-stat"><div class="tm-stat-value tm-numeric">${steps.length}</div><div class="tm-stat-label">${this._t("panel.quest_stat_steps")}</div></div>
          <div class="tm-stat"><div class="tm-stat-value tm-numeric">${this._fmtNum(q.bonus_points || 0)}</div><div class="tm-stat-label">${this._t("panel.quest_stat_bonus", {points_name: pointsName})}</div></div>
        </div>
        <ol class="tm-quest-steps">${stepNames}</ol>
        <div class="tm-meta">${this._t("panel.quest_assigned_to")}: ${this._esc(assigned.join(", "))}</div>
        <div class="tm-card-foot">
          <button type="button" class="tm-btn tm-btn-sm" data-act="edit-quest" data-id="${this._esc(q.id)}">${this._t("panel.btn_edit")}</button>
          <button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="delete-quest" data-id="${this._esc(q.id)}">${this._t("panel.btn_delete")}</button>
        </div>
      </article>
    `;
  }

  // -- Challenges tab ----------------------------------------------------
  _renderChallengesTab() {
    const all = this._state.challenges || [];
    const challenges = this._filterByName(all);
    const pointsName = this._state.settings.points_name || this._t("common.points");
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.challenge_title")} <span class="tm-toolbar-count">${all.length}</span></h2>
        ${all.length > 0 ? this._searchBox(this._t("panel.search_challenges")) : ""}
        <button type="button" class="tm-btn tm-btn-raised" data-act="add-challenge">${this._t("panel.btn_add_challenge")}</button>
      </div>
      ${all.length === 0 ? this._emptyState("🏆", this._t("panel.empty_challenges_title"), this._t("panel.empty_challenges_copy"), "add-challenge", this._t("panel.btn_add_challenge")) :
        challenges.length === 0 ? `<div class="tm-card tm-empty"><p>${this._t("panel.empty_no_match", {kind: this._t("panel.tab_challenges").toLowerCase(), filter: this._esc(this._filter)})}</p></div>` : `
        <div class="tm-grid">
          ${challenges.map(c => this._renderChallengeCard(c, pointsName)).join("")}
          <button type="button" class="tm-add-tile" data-act="add-challenge"><span class="tm-add-plus">＋</span>${this._t("panel.challenge_add_tile")}</button>
        </div>
      `}
    `;
  }

  _renderChallengeCard(c, pointsName) {
    const children = this._state.children || [];
    const assigned = (c.assigned_to && c.assigned_to.length)
      ? children.filter(ch => c.assigned_to.includes(ch.id)).map(ch => ch.name)
      : [this._t("panel.quest_all_children")];
    const scopeLabel = c.scope === "weekly" ? this._t("panel.challenge_scope_weekly") : this._t("panel.challenge_scope_daily");
    const metricLabel = c.metric === "points"
      ? this._t("panel.challenge_metric_points", {points_name: pointsName})
      : this._t("panel.challenge_metric_chores");
    return `
      <article class="tm-card tm-quest-card${c.active ? "" : " tm-quest-inactive"}">
        <div class="tm-child-head">
          <div class="tm-avatar">${this._mdi(c.icon || "mdi:trophy-outline")}</div>
          <div class="tm-child-name">
            <h3>${this._esc(c.name)}
              <span class="tm-pill">${this._esc(scopeLabel)}</span>
              ${c.active ? "" : `<span class="tm-pill tm-pill-muted">${this._t("panel.quest_badge_inactive")}</span>`}
            </h3>
            ${this._idBadge(c.id)}
            <div class="tm-meta">${this._esc(c.description || "")}</div>
          </div>
        </div>
        <div class="tm-stats-row" style="grid-template-columns: 1fr 1fr;">
          <div class="tm-stat"><div class="tm-stat-value tm-numeric">${c.target}</div><div class="tm-stat-label">${this._esc(metricLabel)}</div></div>
          <div class="tm-stat"><div class="tm-stat-value tm-numeric">${this._fmtNum(c.bonus_points || 0)}</div><div class="tm-stat-label">${this._t("panel.quest_stat_bonus", {points_name: pointsName})}</div></div>
        </div>
        <div class="tm-meta">${this._t("panel.quest_assigned_to")}: ${this._esc(assigned.join(", "))}</div>
        <div class="tm-card-foot">
          <button type="button" class="tm-btn tm-btn-sm" data-act="edit-challenge" data-id="${this._esc(c.id)}">${this._t("panel.btn_edit")}</button>
          <button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="delete-challenge" data-id="${this._esc(c.id)}">${this._t("panel.btn_delete")}</button>
        </div>
      </article>
    `;
  }

  // -- Badges tab --------------------------------------------------------
  _renderBadgesTab() {
    const allBadges = this._state.badges || [];
    const sub = this._badgesSubTab || "catalogue";
    const catalogueCount = allBadges.filter(b => b.builtin).length;
    const customCount = allBadges.filter(b => !b.builtin).length;
    const subTabs = [
      { id: "catalogue", label: this._t("panel.badge_subtab_catalogue", { count: catalogueCount }) },
      { id: "custom",    label: this._t("panel.badge_subtab_custom", { count: customCount }) },
      { id: "history",   label: this._t("panel.badge_subtab_history") },
    ];
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.badge_tab_title")} <span class="tm-toolbar-count">${allBadges.length}</span></h2>
        ${sub === "custom" ? `<button type="button" class="tm-btn tm-btn-raised" data-act="add-badge">${this._t("panel.badge_add_btn")}</button>` : ""}
      </div>
      <div class="tm-badge-subtabs">
        ${subTabs.map(s => `<button type="button" class="tm-badge-subtab${sub === s.id ? " tm-badge-subtab-active" : ""}" data-act="badges-subtab" data-sub="${s.id}">${this._esc(s.label)}</button>`).join("")}
      </div>
      ${sub === "catalogue" ? this._renderBadgeCatalogueTab(allBadges) : ""}
      ${sub === "custom"    ? this._renderBadgeCustomTab(allBadges) : ""}
      ${sub === "history"   ? this._renderBadgeHistoryTab() : ""}
    `;
  }

  _tierClass(tier) {
    return `tm-badge-tier-${(tier || "bronze").toLowerCase()}`;
  }

  _criteriaLabel(criteria, combinator = "AND") {
    if (!criteria || !criteria.length) return this._t("badge.manual_award_only");
    const joinKey = combinator === "OR" ? "badge.criteria_or" : "badge.criteria_and";
    return criteria.map(c => `${this._t("badge.criteria_" + c.metric)} ${c.operator} ${c.value}`).join(` ${this._t(joinKey)} `);
  }

  _badgeName(b) {
    if (!b.builtin) return b.name;
    const key = "badge.name_" + b.id.replace("builtin.", "");
    const t = this._t(key);
    return t !== key ? t : b.name;
  }

  _badgeDesc(b) {
    if (!b.builtin) return b.description || "";
    const key = "badge.desc_" + b.id.replace("builtin.", "");
    const t = this._t(key);
    return t !== key ? t : (b.description || "");
  }

  _renderBadgeCatalogueTab(allBadges) {
    const builtins = allBadges.filter(b => b.builtin);
    if (!builtins.length) return `<div class="tm-card tm-empty"><p>${this._t("panel.badge_empty_catalogue")}</p></div>`;
    return `
      <div class="tm-table-wrap">
        <table class="tm-table">
          <thead><tr><th>${this._t("panel.badge_table_badge")}</th><th>${this._t("panel.badge_table_tier")}</th><th>${this._t("panel.badge_table_criteria")}</th><th>${this._t("panel.badge_table_bonus")}</th><th>${this._t("panel.badge_table_enabled")}</th><th></th></tr></thead>
          <tbody>
            ${builtins.map(b => `
              <tr class="tm-row tm-badge-row ${this._tierClass(b.tier)}">
                <td>
                  <div class="tm-badge-icon-sm">${this._mdi(b.icon)}</div>
                  <span style="margin-left:8px"><strong>${this._esc(this._badgeName(b))}</strong>
                  <span class="tm-pill" style="background:var(--tm-surface-2);font-size:10px;margin-left:4px">${this._t("panel.badge_builtin_pill")}</span></span>
                  ${this._badgeDesc(b) ? `<div class="tm-meta">${this._esc(this._badgeDesc(b))}</div>` : ""}
                </td>
                <td><span class="tm-badge-tier-label">${this._t("badge.tier_" + (b.tier || "bronze"))}</span></td>
                <td><code class="tm-badge-criteria">${this._esc(this._criteriaLabel(b.criteria, b.combinator))}</code></td>
                <td><strong class="${b.point_bonus ? "tm-pos" : "tm-text-muted"}">${b.point_bonus ? `+${b.point_bonus}` : "—"}</strong></td>
                <td>
                  <button type="button" class="tm-badge-toggle${b.enabled !== false ? " tm-badge-toggle-on" : ""}"
                    data-act="toggle-badge-enabled" data-id="${this._esc(b.id)}" data-enabled="${b.enabled !== false}"
                    title="${b.enabled !== false ? this._t("panel.badge_toggle_disable") : this._t("panel.badge_toggle_enable")}">
                  </button>
                </td>
                <td class="tm-row-actions"><div>
                  <button type="button" class="tm-icon-btn" data-act="edit-badge" data-id="${this._esc(b.id)}" title="${this._t("panel.badge_edit_btn")}">✏</button>
                </div></td>
              </tr>
            `).join("")}
          </tbody>
        </table>
      </div>
    `;
  }

  _renderBadgeCustomTab(allBadges) {
    const customs = allBadges.filter(b => !b.builtin);
    if (!customs.length) return `
      <div class="tm-card tm-empty" style="text-align:center;padding:32px">
        <p style="font-size:24px;margin-bottom:8px">🏅</p>
        <p style="font-weight:600;margin-bottom:4px">${this._t("panel.badge_empty_custom_title")}</p>
        <p class="tm-meta" style="margin-bottom:16px">${this._t("panel.badge_empty_custom_desc")}</p>
        <button type="button" class="tm-btn tm-btn-raised" data-act="add-badge">${this._t("panel.badge_add_btn")}</button>
      </div>`;
    return `
      <div class="tm-table-wrap">
        <table class="tm-table">
          <thead><tr><th>${this._t("panel.badge_table_badge")}</th><th>${this._t("panel.badge_table_tier")}</th><th>${this._t("panel.badge_table_criteria")}</th><th>${this._t("panel.badge_table_bonus")}</th><th>${this._t("panel.badge_table_enabled")}</th><th></th></tr></thead>
          <tbody>
            ${customs.map(b => `
              <tr class="tm-row tm-badge-row ${this._tierClass(b.tier)}">
                <td>
                  <div class="tm-badge-icon-sm">${this._mdi(b.icon || "mdi:medal")}</div>
                  <span style="margin-left:8px"><strong>${this._esc(b.name)}</strong></span>
                  ${b.description ? `<div class="tm-meta">${this._esc(b.description)}</div>` : ""}
                </td>
                <td><span class="tm-badge-tier-label">${this._t("badge.tier_" + (b.tier || "bronze"))}</span></td>
                <td><code class="tm-badge-criteria">${this._esc(this._criteriaLabel(b.criteria, b.combinator))}</code></td>
                <td><strong class="${b.point_bonus ? "tm-pos" : "tm-text-muted"}">${b.point_bonus ? `+${b.point_bonus}` : "—"}</strong></td>
                <td>
                  <button type="button" class="tm-badge-toggle${b.enabled !== false ? " tm-badge-toggle-on" : ""}"
                    data-act="toggle-badge-enabled" data-id="${this._esc(b.id)}" data-enabled="${b.enabled !== false}"
                    title="${b.enabled !== false ? this._t("panel.badge_toggle_disable") : this._t("panel.badge_toggle_enable")}">
                  </button>
                </td>
                <td class="tm-row-actions"><div>
                  <button type="button" class="tm-btn tm-btn-sm" data-act="award-badge" data-id="${this._esc(b.id)}">${this._t("panel.badge_award_btn")}</button>
                  <button type="button" class="tm-icon-btn" data-act="edit-badge" data-id="${this._esc(b.id)}" title="${this._t("panel.badge_edit_btn")}">✏</button>
                  <button type="button" class="tm-icon-btn" data-act="delete-badge" data-id="${this._esc(b.id)}" title="${this._t("panel.badge_delete_btn")}">🗑</button>
                </div></td>
              </tr>
            `).join("")}
          </tbody>
        </table>
      </div>
    `;
  }

  _renderBadgeHistoryTab() {
    const awards = (this._state.awarded_badges || []).slice().sort((a, b) => (b.earned_at || "").localeCompare(a.earned_at || ""));
    const badgeById = Object.fromEntries((this._state.badges || []).map(b => [b.id, b]));
    const childById = Object.fromEntries((this._state.children || []).map(c => [c.id, c]));
    const rebuildBtn = `<button type="button" class="tm-btn" data-act="rebuild-badges" title="${this._t("panel.badge_rebuild_tooltip")}">${this._t("panel.badge_rebuild_btn")}</button>`;
    if (!awards.length) return `
      <div style="display:flex;justify-content:flex-end;margin-bottom:12px">${rebuildBtn}</div>
      <div class="tm-card tm-empty"><p>${this._t("panel.badge_empty_history")}</p></div>`;
    return `
      <div style="display:flex;justify-content:flex-end;margin-bottom:12px">${rebuildBtn}</div>
      <div class="tm-table-wrap">
        <table class="tm-table">
          <thead><tr><th>${this._t("panel.badge_table_badge")}</th><th>${this._t("panel.badge_table_child")}</th><th>${this._t("panel.badge_table_when")}</th><th>${this._t("panel.badge_table_source")}</th><th></th></tr></thead>
          <tbody>
            ${awards.map(a => {
              const badge = badgeById[a.badge_id] || {};
              const child = childById[a.child_id] || {};
              const tierCls = this._tierClass(badge.tier || "bronze");
              const srcLabel = a.silent ? this._t("badge.source_silent") : a.manually_awarded ? this._t("badge.source_manual") : this._t("badge.source_auto");
              const srcCls = a.silent ? "tm-badge-src-silent" : a.manually_awarded ? "tm-badge-src-manual" : "tm-badge-src-auto";
              const when = a.earned_at ? new Date(a.earned_at).toLocaleString() : "—";
              return `
                <tr class="tm-row tm-badge-row ${tierCls}">
                  <td>
                    <div class="tm-badge-icon-sm">${this._mdi(badge.icon || "mdi:medal")}</div>
                    <span style="margin-left:8px"><strong>${this._esc(this._badgeName(badge) || a.badge_id)}</strong></span>
                  </td>
                  <td><strong style="color:var(--tm-accent)">${this._esc(child.name || a.child_id)}</strong></td>
                  <td class="tm-meta">${this._esc(when)}</td>
                  <td><span class="tm-badge-src ${srcCls}">${srcLabel}</span></td>
                  <td class="tm-row-actions"><div>
                    <button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="revoke-badge" data-id="${this._esc(a.id)}" data-name="${this._esc(this._badgeName(badge) || "badge")}">${this._t("panel.badge_revoke_btn")}</button>
                  </div></td>
                </tr>
              `;
            }).join("")}
          </tbody>
        </table>
      </div>
    `;
  }

  // Badge dialog open/save/delete
  _openBadgeDialog(id) {
    const blank = { name: "", description: "", icon: "mdi:medal", tier: "bronze", point_bonus: 0, notify_on_earn: true, assigned_to: [], criteria: [], builtin: false };
    if (id) {
      const b = (this._state.badges || []).find(x => x.id === id);
      if (!b) return;
      this._openDialog({ kind: "badge", mode: "edit", data: { ...blank, ...b, assigned_to: [...(b.assigned_to || [])], criteria: (b.criteria || []).map(c => ({ ...c })) } });
    } else {
      this._openDialog({ kind: "badge", mode: "add", data: { ...blank } });
    }
  }

  async _doSaveBadge() {
    this._syncIconPickers();
    const d = this._dialog.data;
    if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_badge_name_required")); return; }
    // validate criteria values
    for (const c of (d.criteria || [])) {
      if (!BADGE_BOOL_METRICS.includes(c.metric) && (!c.value || Number(c.value) < 1)) {
        this._showToast("err", this._t("panel.toast_badge_criterion_invalid")); return;
      }
    }
    const wasAdd = this._dialog.mode === "add";
    const base = {
      name: d.name.trim(),
      description: d.description || "",
      icon: d.icon || "mdi:medal",
      tier: d.tier || "bronze",
      point_bonus: Number(d.point_bonus) || 0,
      notify_on_earn: d.notify_on_earn !== false && d.notify_on_earn !== "false",
      assigned_to: d.assigned_to || [],
      criteria: (d.criteria || []).map(c => ({
        metric: c.metric,
        operator: BADGE_BOOL_METRICS.includes(c.metric) ? ">=" : (c.operator || ">="),
        value: BADGE_BOOL_METRICS.includes(c.metric) ? 1 : Number(c.value),
      })),
      combinator: d.combinator === "OR" ? "OR" : "AND",
    };
    let ok, err;
    if (wasAdd) {
      ({ ok, err } = await this._callService("add_badge", base));
    } else {
      ({ ok, err } = await this._callService("update_badge", { badge_id: d.id, ...base }));
    }
    if (!ok) { this._showToast("err", this._t("panel.toast_badge_save_failed", { error: err })); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", wasAdd ? this._t("panel.toast_badge_added") : this._t("panel.toast_badge_updated"));
  }

  async _confirmDeleteBadge(id) {
    const b = (this._state.badges || []).find(x => x.id === id);
    if (!b) return;
    if (!confirm(this._t("panel.badge_confirm_delete", { name: b.name }))) return;
    const { ok, err } = await this._callService("remove_badge", { badge_id: id });
    if (!ok) { this._showToast("err", this._t("panel.toast_badge_delete_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_badge_deleted"));
  }

  async _doToggleBadgeEnabled(id, currentlyEnabled) {
    const { ok, err } = await this._callService("update_badge", { badge_id: id, enabled: !currentlyEnabled });
    if (!ok) { this._showToast("err", this._t("panel.toast_badge_update_failed", { error: err })); return; }
    await this._fetchState();
  }

  _openAwardDialog(badgeId) {
    const b = (this._state.badges || []).find(x => x.id === badgeId);
    if (!b) return;
    this._openDialog({ kind: "award-badge", mode: "award", data: { badge_id: badgeId, badge_name: b.name, child_id: "" } });
  }

  async _doAwardBadge(badgeId, childId) {
    if (!childId) { this._showToast("err", this._t("panel.toast_badge_select_child")); return; }
    const { ok, err } = await this._callService("award_badge_manually", { badge_id: badgeId, child_id: childId });
    if (!ok) { this._showToast("err", this._t("panel.toast_badge_award_failed", { error: err })); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_badge_awarded"));
  }

  async _doRevokeBadge(awardedBadgeId, badgeName) {
    if (!confirm(this._t("panel.badge_confirm_revoke", { name: badgeName }))) return;
    const { ok, err } = await this._callService("revoke_badge", { awarded_badge_id: awardedBadgeId });
    if (!ok) { this._showToast("err", this._t("panel.toast_badge_revoke_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_badge_revoked"));
  }

  async _doRebuildBadges() {
    if (!confirm(this._t("panel.badge_confirm_rebuild"))) return;
    const { ok, err } = await this._callService("rebuild_badges", {});
    if (!ok) { this._showToast("err", this._t("panel.toast_badge_rebuild_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.toast_badge_rebuilt"));
  }

  async _callService(service, data = {}) {
    try {
      await this._hass.callService("taskmate", service, data);
      return { ok: true };
    } catch (err) {
      return { ok: false, err: (err && err.message) || String(err) };
    }
  }

  // Badge dialog renders
  _renderBadgeDialog() {
    const d = this._dialog.data;
    const isBuiltin = !!d.builtin;
    const children = this._state.children || [];
    const title = this._dialog.mode === "add" ? this._t("panel.badge_dialog_add_title") : this._t("panel.badge_dialog_edit_title", { name: d.name });
    const criteria = d.criteria || [];
    return this._dialogShell(title,
      [
        isBuiltin ? `<div class="tm-field"><span class="tm-pill" style="background:var(--tm-surface-2)">${this._t("panel.badge_builtin_hint")}</span></div>` : "",
        !isBuiltin ? this._field(this._t("panel.badge_name_label"), "name", d.name, "text") : "",
        !isBuiltin ? this._field(this._t("panel.badge_description_label"), "description", d.description, "text") : "",
        `<div class="tm-field-row">
          ${!isBuiltin ? this._iconPickerField(this._t("panel.badge_icon_label"), "icon", d.icon) : ""}
          ${this._select(this._t("panel.badge_tier_label"), "tier", d.tier || "bronze", BADGE_TIERS)}
        </div>`,
        `<div class="tm-field-row">
          ${this._field(this._t("panel.badge_bonus_label"), "point_bonus", d.point_bonus || 0, "number")}
          ${this._select(this._t("panel.badge_notify_label"), "notify_on_earn", d.notify_on_earn !== false ? "true" : "false", [{ v: "true", l: this._t("panel.badge_notify_yes") }, { v: "false", l: this._t("panel.badge_notify_no") }])}
        </div>`,
        children.length > 0 ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.badge_assigned_label")}</span>
            <div class="tm-chip-row">
              ${children.map(c => `
                <button type="button" class="tm-chip-btn ${(d.assigned_to || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="badge-toggle-assigned" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
            <span class="tm-field-hint">${this._t("panel.badge_assigned_hint")}</span>
          </div>
        ` : "",
        !isBuiltin ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.badge_criteria_label")}</span>
            <div class="tm-badge-criteria-block">
              ${criteria.length === 0 ? `<p class="tm-field-hint" style="margin:0 0 8px">${this._t("panel.badge_no_criteria_hint")}</p>` : ""}
              ${criteria.map((c, idx) => {
                const isBool = BADGE_BOOL_METRICS.includes(c.metric);
                return `
                  <div class="tm-badge-criterion-row">
                    <select class="tm-select" data-badge-criterion-field="metric" data-badge-criterion-idx="${idx}">
                      ${BADGE_METRICS.map(m => `<option value="${m.v}"${c.metric === m.v ? " selected" : ""}>${this._t(m.lk)}</option>`).join("")}
                    </select>
                    ${isBool ? `<span class="tm-badge-op">=</span>` : `
                    <select class="tm-select tm-badge-op-select" data-badge-criterion-field="operator" data-badge-criterion-idx="${idx}">
                      ${BADGE_OPERATORS.map(sym => { const v = BADGE_OP_VALUES[sym]; return `<option value="${v}"${(c.operator || ">=") === v ? " selected" : ""}>${sym}</option>`; }).join("")}
                    </select>`}
                    ${isBool ? `<span class="tm-field-hint" style="flex:1">true</span>` : `<input class="tm-input" type="number" min="0" value="${c.value || 1}" data-badge-criterion-field="value" data-badge-criterion-idx="${idx}">`}
                    <button type="button" class="tm-icon-btn" data-act="badge-remove-criterion" data-idx="${idx}" title="${this._t("panel.badge_remove_criterion_btn")}">✕</button>
                  </div>
                `;
              }).join("")}
              <button type="button" class="tm-btn" data-act="badge-add-criterion" style="margin-top:6px;width:100%;border-style:dashed">${this._t("panel.badge_add_criterion_btn")}</button>
              ${criteria.length >= 2 ? `
                <div class="tm-badge-combinator">
                  <span class="tm-field-hint">${this._t("panel.badge_combinator_label")}</span>
                  <select class="tm-select" data-field="combinator">
                    <option value="AND"${(d.combinator || "AND") !== "OR" ? " selected" : ""}>${this._t("panel.badge_combinator_all")}</option>
                    <option value="OR"${d.combinator === "OR" ? " selected" : ""}>${this._t("panel.badge_combinator_any")}</option>
                  </select>
                </div>
              ` : ""}
            </div>
          </div>
        ` : "",
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.badge_cancel_btn")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-badge">${this._t("panel.badge_save_btn")}</button>`
    );
  }

  _renderAwardDialog() {
    const d = this._dialog.data;
    const children = this._state.children || [];
    return this._dialogShell(this._t("panel.badge_dialog_award_title", { name: d.badge_name }),
      `<div class="tm-field">
        <span class="tm-field-label">${this._t("panel.badge_award_to_label")}</span>
        <select class="tm-select" data-field="child_id">
          <option value="">${this._t("panel.badge_award_select_child")}</option>
          ${children.map(c => `<option value="${this._esc(c.id)}">${this._esc(c.name)}</option>`).join("")}
        </select>
      </div>`,
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.badge_cancel_btn")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="do-award-badge" data-id="${this._esc(d.badge_id)}">${this._t("panel.badge_award_btn")}</button>`
    );
  }

  // -- Penalties / Bonuses tab -------------------------------------------
  _renderPenBonTab(kind) {
    const allItems = (kind === "penalty" ? this._state.penalties : this._state.bonuses) || [];
    const items = this._filterByName(allItems);
    const childById = Object.fromEntries((this._state.children || []).map(c => [c.id, c]));
    const labels = kind === "penalty" ? { plural: this._t("panel.penalty_title"), add: this._t("panel.btn_add_penalty"), icon: "⚠️" } : { plural: this._t("panel.bonus_title"), add: this._t("panel.btn_add_bonus"), icon: "⭐" };
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${labels.plural} <span class="tm-toolbar-count">${allItems.length}</span></h2>
        ${allItems.length > 0 ? this._searchBox(kind === "penalty" ? this._t("panel.search_penalties") : this._t("panel.search_bonuses")) : ""}
        <button type="button" class="tm-btn tm-btn-raised" data-act="add-${kind}">${labels.add}</button>
      </div>
      ${allItems.length === 0 ? this._emptyState(labels.icon, kind === "penalty" ? this._t("panel.penalty_empty_title") : this._t("panel.bonus_empty_title"), kind === "penalty" ? this._t("panel.penalty_empty_description") : this._t("panel.bonus_empty_description"), `add-${kind}`, labels.add) :
        items.length === 0 ? `<div class="tm-card tm-empty"><p>${this._t("panel.empty_no_match", {kind: labels.plural.toLowerCase(), filter: this._esc(this._filter)})}</p></div>` : `
        <div class="tm-table-wrap">
          <table class="tm-table">
            <thead><tr><th>${this._t("panel.penbon_table_name")}</th><th>${this._t("panel.penbon_table_points")}</th><th>${this._t("panel.penbon_table_assigned")}</th><th></th></tr></thead>
            <tbody>
              ${items.map(item => {
                const assignedNames = (item.assigned_to || []).length === 0
                  ? `<span class="tm-text-muted">${this._t("panel.common_all_children")}</span>`
                  : this._esc((item.assigned_to || []).map(id => (childById[id] && childById[id].name) || "?").join(", "));
                return `
                  <tr class="tm-row">
                    <td><span class="tm-row-icon">${this._mdi(item.icon)}</span><strong>${this._esc(item.name)}</strong>${this._idBadge(item.id)}${item.description ? `<div class="tm-meta">${this._esc(item.description)}</div>` : ""}</td>
                    <td><strong class="tm-numeric ${kind === "penalty" ? "tm-neg" : "tm-pos"}">${kind === "penalty" ? "−" : "+"}${item.points}</strong></td>
                    <td>${assignedNames}</td>
                    <td class="tm-row-actions"><div>
                      <button type="button" class="tm-btn tm-btn-sm" data-act="apply-${kind}" data-id="${this._esc(item.id)}">${this._t("panel.btn_apply")}</button>
                      <button type="button" class="tm-icon-btn" data-act="edit-${kind}" data-id="${this._esc(item.id)}" title="${this._t("panel.btn_edit")}">✏</button>
                      <button type="button" class="tm-icon-btn" data-act="delete-${kind}" data-id="${this._esc(item.id)}" title="${this._t("panel.btn_delete")}">🗑</button>
                    </div></td>
                  </tr>
                `;
              }).join("")}
            </tbody>
          </table>
        </div>
      `}
    `;
  }

  // -- Groups tab --------------------------------------------------------
  _renderGroupsTab() {
    const all = this._state.task_groups || [];
    const groups = this._filterByName(all);
    const choreById = Object.fromEntries((this._state.chores || []).map(c => [c.id, c]));
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.group_title")} <span class="tm-toolbar-count">${all.length}</span></h2>
        ${all.length > 0 ? this._searchBox(this._t("panel.search_groups")) : ""}
        <button type="button" class="tm-btn tm-btn-raised" data-act="add-group">${this._t("panel.btn_add_group")}</button>
      </div>
      ${all.length === 0 ? this._emptyState("🔗", this._t("panel.empty_groups_title"), this._t("panel.empty_groups_copy"), "add-group", this._t("panel.btn_add_group")) :
        groups.length === 0 ? `<div class="tm-card tm-empty"><p>${this._t("panel.empty_no_match", {kind: this._t("panel.tab_groups").toLowerCase(), filter: this._esc(this._filter)})}</p></div>` : `
        <div class="tm-grid">
          ${groups.map(g => `
            <article class="tm-card">
              <div class="tm-child-head">
                <div class="tm-avatar"><ha-icon icon="${g.policy === 'sticky' ? 'mdi:link-variant' : 'mdi:arrow-split-horizontal'}"></ha-icon></div>
                <div class="tm-child-name">
                  <h3>${this._esc(g.name)}</h3>
                  ${this._idBadge(g.id)}
                  <div class="tm-meta"><span class="tm-pill tm-pill-${g.policy}">${this._t(`panel.group_policy_${g.policy}_short`)}</span> · ${this._t("panel.group_chore_count", {count: (g.chore_ids || []).length})}</div>
                </div>
              </div>
              <ul class="tm-group-list">
                ${(g.chore_ids || []).map(id => `<li>${this._esc((choreById[id] && choreById[id].name) || this._t("panel.group_missing_chore", {id}))}</li>`).join("")}
              </ul>
              <div class="tm-card-foot">
                <button type="button" class="tm-btn tm-btn-sm" data-act="edit-group" data-id="${this._esc(g.id)}">${this._t("panel.btn_edit")}</button>
                <button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="delete-group" data-id="${this._esc(g.id)}">${this._t("panel.btn_delete")}</button>
              </div>
            </article>
          `).join("")}
          <button type="button" class="tm-add-tile" data-act="add-group"><span class="tm-add-plus">＋</span>${this._t("panel.group_add_tile")}</button>
        </div>
      `}
    `;
  }

  // -- Templates tab ---------------------------------------------------------
  _renderTemplatesTab() {
    if (this._templateView === "picker") return this._renderTemplatePicker();
    if (this._templateView === "preview") return this._renderTemplatePreview();
    return this._renderTemplateManagement();
  }

  _renderTemplateManagement() {
    const templates = this._state.templates || [];
    const custom = templates.filter(t => !t.builtin);
    const builtin = templates.filter(t => t.builtin);
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.template_title")} <span class="tm-toolbar-count">${templates.length}</span></h2>
        <span style="flex:1"></span>
        <button type="button" class="tm-btn tm-btn-raised" data-act="tpl-create">${this._t("panel.btn_create_template_toolbar")}</button>
      </div>
      ${custom.length > 0 ? `
        <div class="tm-section-divider">${this._t("panel.template_custom_templates")}</div>
        ${custom.map(t => this._renderManageTemplateCard(t, false)).join("")}
      ` : ""}
      <div class="tm-section-divider">${custom.length > 0 ? this._t("panel.template_builtin_templates") : this._t("panel.template_builtin_packs")}</div>
      ${builtin.map(t => this._renderManageTemplateCard(t, true)).join("")}
    `;
  }

  _renderManageTemplateCard(tpl, locked) {
    const count = (tpl.chores || []).length;
    const pts = (tpl.chores || []).reduce((s, c) => s + (c.points || 0), 0);
    return `
      <div class="tm-manage-tpl ${locked ? "tm-manage-tpl-locked" : ""}" data-act="tpl-select" data-id="${this._esc(tpl.id)}" style="cursor:pointer">
        <div class="tm-tpl-icon"><ha-icon icon="${this._esc(tpl.icon || "mdi:clipboard-list")}"></ha-icon></div>
        <div class="tm-manage-tpl-info">
          <div class="tm-manage-tpl-name">${this._esc(tpl.name)}${locked ? ' <span class="tm-text-vfaint" style="font-size:12px">🔒</span>' : ""}</div>
          <div class="tm-meta">${this._t("panel.template_pts_total", {count, points: pts})}</div>
        </div>
        <div class="tm-row-actions">
          ${locked ? "" : `<button type="button" class="tm-btn tm-btn-sm" data-act="tpl-edit" data-id="${this._esc(tpl.id)}">${this._t("panel.btn_edit")}</button>`}
          ${locked ? "" : `<button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="tpl-delete" data-id="${this._esc(tpl.id)}">${this._t("panel.btn_delete")}</button>`}
        </div>
      </div>
    `;
  }

  _renderTemplatePicker() {
    const templates = this._state.templates || [];
    const builtin = templates.filter(t => t.builtin);
    const custom = templates.filter(t => !t.builtin);
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.template_picker_title")}</h2>
        <span style="flex:1"></span>
        <button type="button" class="tm-btn" data-act="tpl-back">${this._t("panel.btn_cancel")}</button>
      </div>
      <div class="tm-section-divider">${this._t("panel.template_builtin_packs")}</div>
      <div class="tm-grid">
        ${builtin.map(t => this._renderTemplatePickerCard(t)).join("")}
      </div>
      ${custom.length > 0 ? `
        <div class="tm-section-divider">${this._t("panel.template_custom_packs")}</div>
        <div class="tm-grid">
          ${custom.map(t => this._renderTemplatePickerCard(t)).join("")}
        </div>
      ` : ""}
    `;
  }

  _renderTemplatePickerCard(tpl) {
    const count = (tpl.chores || []).length;
    const pts = (tpl.chores || []).reduce((s, c) => s + (c.points || 0), 0);
    return `
      <div class="tm-card tm-tpl-picker-card" data-act="tpl-select" data-id="${this._esc(tpl.id)}" style="margin-bottom:0">
        <div class="tm-tpl-picker-head">
          <div class="tm-tpl-icon"><ha-icon icon="${this._esc(tpl.icon || "mdi:clipboard-list")}"></ha-icon></div>
          <div style="flex:1">
            <div class="tm-tpl-picker-name">${this._esc(tpl.name)}</div>
            <div class="tm-meta">${this._t("panel.template_pts_total", {count, points: pts})}</div>
          </div>
          <span class="tm-pill ${tpl.builtin ? "tm-pill-accent" : "tm-pill-success"}">${tpl.builtin ? this._t("panel.template_builtin") : this._t("panel.template_custom")}</span>
        </div>
        <div class="tm-tpl-chore-pills">
          ${(tpl.chores || []).map(c => `<span class="tm-tpl-chore-pill">${this._esc(c.name)}</span>`).join("")}
        </div>
      </div>
    `;
  }

  _renderTemplatePreview() {
    const tpl = this._templateSelected;
    if (!tpl) return "";
    const chores = this._templateChores;
    const totalPts = chores.reduce((s, c) => s + (c.points || 0), 0);
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._esc(tpl.name)}</h2>
        <span class="tm-pill ${tpl.builtin ? "tm-pill-accent" : "tm-pill-success"}" style="margin-left:8px">${tpl.builtin ? this._t("panel.template_builtin") : this._t("panel.template_custom")}</span>
        <span style="flex:1"></span>
        <button type="button" class="tm-btn" data-act="tpl-back">${this._t("panel.btn_back")}</button>
      </div>
      <p class="tm-meta" style="margin-bottom:16px">${this._t("panel.template_preview_hint")}</p>
      ${chores.map((c, i) => this._renderTemplateChoreCard(c, i)).join("")}
      ${chores.length > 0 ? `
        <div class="tm-tpl-confirm-bar">
          <div>${this._t("panel.template_confirm_bar", {count: chores.length, points: totalPts})}</div>
          <button type="button" class="tm-btn tm-btn-raised" data-act="tpl-apply">${this._t("panel.template_create_btn", {count: chores.length})}</button>
        </div>
      ` : `<div class="tm-card tm-empty"><p>${this._t("panel.empty_template_all_removed")}</p></div>`}
    `;
  }

  _renderTemplateChoreCard(chore, idx) {
    const expanded = chore._expanded;
    const schedLabel = (chore.due_days || []).length === 0 ? this._t("panel.common_daily") : (chore.due_days || []).map(d => this._labelOf(DAYS, d)).join(", ");
    return `
      <div class="tm-tpl-preview-card">
        <div class="tm-tpl-preview-header" data-act="tpl-toggle-expand" data-idx="${idx}">
          <span class="tm-tpl-expand ${expanded ? "open" : ""}">▶</span>
          <span class="tm-tpl-preview-name">${this._esc(chore.name)}</span>
          <span class="tm-tpl-preview-summary">
            <span>${this._t("panel.pts_display", {count: chore.points || 0})}</span>
            <span>${schedLabel}</span>
            <span>${this._esc(this._timeCategoryLabel(chore.time_category))}</span>
          </span>
          <button type="button" class="tm-tpl-remove" data-act="tpl-remove-chore" data-idx="${idx}" title="${this._t("panel.tooltip_remove_batch")}">✕</button>
        </div>
        ${expanded ? `
          <div class="tm-tpl-preview-body">
            <div class="tm-field-row">
              <div class="tm-field"><span class="tm-field-label">${this._t("panel.common_name")}</span><input class="tm-input" type="text" data-tpl-field="name" data-tpl-idx="${idx}" value="${this._esc(chore.name)}"></div>
              <div class="tm-field"><span class="tm-field-label">${this._t("panel.common_points")}</span><input class="tm-input" type="number" data-tpl-field="points" data-tpl-idx="${idx}" value="${chore.points || 0}" min="0"></div>
            </div>
            <div class="tm-field-row">
              <div class="tm-field"><span class="tm-field-label">${this._t("panel.chore_time_category_label")}</span>
                <select class="tm-select" data-tpl-field="time_category" data-tpl-idx="${idx}">
                  ${this._timeCategoryOptions().map(t => `<option value="${this._esc(t.v)}" ${chore.time_category === t.v ? "selected" : ""}>${this._esc(t.l)}</option>`).join("")}
                </select>
              </div>
              <div class="tm-field"><span class="tm-field-label">${this._t("panel.template_assignment_mode_label")}</span>
                <select class="tm-select" data-tpl-field="assignment_mode" data-tpl-idx="${idx}">
                  ${ASSIGNMENT_MODES.map(m => `<option value="${m.v}" ${chore.assignment_mode === m.v ? "selected" : ""}>${this._t(m.lk).split(" — ")[0]}</option>`).join("")}
                </select>
              </div>
            </div>
            <div class="tm-field" style="margin-bottom:12px"><span class="tm-field-label">${this._t("panel.template_schedule_days_label")}</span>
              <div class="tm-day-pills">
                ${DAYS.map(d => `<span class="tm-day-pill ${(chore.due_days || []).includes(d.v) ? "active" : ""}" data-act="tpl-toggle-day" data-idx="${idx}" data-day="${d.v}">${this._t(d.lk)}</span>`).join("")}
              </div>
            </div>
            <div class="tm-field-row">
              <div class="tm-field"><span class="tm-field-label">${this._t("panel.template_requires_approval_label")}</span>
                <select class="tm-select" data-tpl-field="requires_approval" data-tpl-idx="${idx}">
                  <option value="false" ${!chore.requires_approval ? "selected" : ""}>${this._t("panel.template_requires_approval_no")}</option>
                  <option value="true" ${chore.requires_approval ? "selected" : ""}>${this._t("panel.template_requires_approval_yes")}</option>
                </select>
              </div>
              <div class="tm-field"><span class="tm-field-label">${this._t("panel.template_daily_limit_label")}</span>
                <input class="tm-input" type="number" data-tpl-field="daily_limit" data-tpl-idx="${idx}" value="${chore.daily_limit || 1}" min="1">
              </div>
            </div>
          </div>
        ` : ""}
      </div>
    `;
  }

  _renderSaveTemplateDialog() {
    if (!this._saveTemplateDialog) return "";
    const chores = this._state.chores || [];
    return `
      <div class="tm-scrim" data-act="scrim">
        <div class="tm-dialog">
          <div class="tm-dialog-head">
            <h2>${this._t("panel.template_save_title")}</h2>
            <button type="button" class="tm-icon-btn" data-act="tpl-save-cancel">✕</button>
          </div>
          <div class="tm-dialog-body">
            <div class="tm-field" style="margin-bottom:14px"><span class="tm-field-label">${this._t("panel.template_save_name_label")}</span><input class="tm-input" type="text" data-field="tpl_save_name" placeholder="${this._t("panel.template_save_name_placeholder")}"></div>
            <div class="tm-field" style="margin-bottom:14px"><span class="tm-field-label">${this._t("panel.template_save_icon_label")}</span><input class="tm-input" type="text" data-field="tpl_save_icon" placeholder="mdi:clipboard-list" value="mdi:clipboard-list"></div>
            <label class="tm-field-label" style="display:block;margin-bottom:8px">${this._t("panel.template_save_select_chores")}</label>
            <div class="tm-tpl-checklist">
              ${chores.map(c => `
                <label class="tm-tpl-check-row">
                  <input type="checkbox" data-tpl-chore-check="${this._esc(c.id)}">
                  <span>${this._esc(c.name)}</span>
                  <span class="tm-text-muted" style="margin-left:auto">${this._t("panel.pts_display", {count: c.points})}</span>
                </label>
              `).join("")}
            </div>
          </div>
          <div class="tm-dialog-foot">
            <button type="button" class="tm-btn" data-act="tpl-save-cancel">${this._t("panel.btn_cancel")}</button>
            <button type="button" class="tm-btn tm-btn-raised" data-act="tpl-save-confirm">${this._t("panel.btn_save_template")}</button>
          </div>
        </div>
      </div>
    `;
  }

  _selectTemplate(templateId) {
    const templates = this._state.templates || [];
    const tpl = templates.find(t => t.id === templateId);
    if (!tpl) return;
    this._templateSelected = tpl;
    this._templateChores = (tpl.chores || []).map(c => ({ ...c, _expanded: false }));
    this._templateView = "preview";
    this._render();
  }

  async _doApplyTemplate() {
    const chores = this._templateChores.map(c => {
      const { _expanded, ...rest } = c;
      return rest;
    });
    if (chores.length === 0) return;
    const { ok, err } = await this._callWS({ type: "taskmate/templates/apply", chores });
    if (ok) {
      this._showToast("success", this._t("panel.toast_created_chores", {count: chores.length}));
      this._templateView = null;
      this._templateSelected = null;
      this._templateChores = [];
      await this._fetchState();
      this._activeTab = "chores";
      this._render();
    } else {
      this._showToast("error", err || this._t("panel.toast_template_failed_apply"));
    }
  }

  async _doSaveFromChores() {
    const name = this.querySelector('[data-field="tpl_save_name"]')?.value?.trim();
    const icon = this.querySelector('[data-field="tpl_save_icon"]')?.value?.trim() || "mdi:clipboard-list";
    const checkboxes = this.querySelectorAll('[data-tpl-chore-check]:checked');
    const chore_ids = Array.from(checkboxes).map(cb => cb.dataset.tplChoreCheck);
    if (!name || chore_ids.length === 0) {
      this._showToast("error", this._t("panel.toast_name_enter_select"));
      return;
    }
    const { ok, err } = await this._callWS({ type: "taskmate/templates/save_from_chores", chore_ids, name, icon });
    if (ok) {
      this._showToast("success", this._t("panel.toast_template_saved", {name}));
      this._saveTemplateDialog = false;
      await this._fetchState();
      this._render();
    } else {
      this._showToast("error", err || this._t("panel.toast_template_failed_save"));
    }
  }

  async _confirmDeleteTemplate(templateId) {
    const templates = this._state.templates || [];
    const tpl = templates.find(t => t.id === templateId);
    if (!tpl) return;
    if (!confirm(this._t("panel.confirm_delete_template", {name: tpl.name}))) return;
    const { ok, err } = await this._callWS({ type: "taskmate/templates/delete", template_id: templateId });
    if (ok) {
      this._showToast("success", this._t("panel.toast_template_deleted"));
      await this._fetchState();
    } else {
      this._showToast("error", err || this._t("panel.toast_template_failed_delete"));
    }
  }

  _openCreateTemplateDialog() {
    this._openDialog({
      kind: "create-template",
      data: { name: "", icon: "mdi:clipboard-list", chores: [{ name: "", points: 5, time_category: "anytime", schedule_mode: "specific_days", due_days: [], requires_approval: false, assignment_mode: "everyone", daily_limit: 1, completion_sound: "coin" }] },
    });
  }

  _openEditTemplateDialog(templateId) {
    const templates = this._state.templates || [];
    const tpl = templates.find(t => t.id === templateId);
    if (!tpl) return;
    this._openDialog({
      kind: "edit-template",
      data: { template_id: templateId, name: tpl.name, icon: tpl.icon || "mdi:clipboard-list", chores: (tpl.chores || []).map(c => ({ ...c })) },
    });
  }

  async _doSaveCreatedTemplate() {
    const d = this._dialog?.data;
    if (!d || !d.name?.trim() || !d.chores?.length) { this._showToast("error", this._t("panel.toast_name_and_chore_required")); return; }
    const { ok, err } = await this._callWS({ type: "taskmate/templates/create", name: d.name.trim(), icon: d.icon || "mdi:clipboard-list", chores: d.chores });
    if (ok) { this._showToast("success", this._t("panel.toast_template_created", {name: d.name})); this._closeDialog(true); await this._fetchState(); }
    else { this._showToast("error", err || this._t("panel.toast_template_failed_create")); }
  }

  async _doSaveEditedTemplate() {
    const d = this._dialog?.data;
    if (!d || !d.template_id || !d.name?.trim()) { this._showToast("error", this._t("panel.toast_name_required_template")); return; }
    const { ok, err } = await this._callWS({ type: "taskmate/templates/update", template_id: d.template_id, name: d.name.trim(), icon: d.icon || "mdi:clipboard-list", chores: d.chores || [] });
    if (ok) { this._showToast("success", this._t("panel.toast_template_updated")); this._closeDialog(true); await this._fetchState(); }
    else { this._showToast("error", err || this._t("panel.toast_template_failed_update")); }
  }

  async _doUndoTransaction(id) {
    if (!id) return;
    if (!confirm(this._t("panel.activity_undo_confirm"))) return;
    const { ok, err } = await this._callWS({ type: "taskmate/undo_transaction", transaction_id: id });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.activity_undone"));
  }

  // -- Audit tab ---------------------------------------------------------
  async _doClearAudit() {
    if (!confirm(this._t("panel.audit_clear_confirm"))) return;
    const { ok, err } = await this._callWS({ type: "taskmate/audit/clear" });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t("panel.audit_cleared"));
  }

  _renderAuditTab() {
    const entries = this._state.audit_log || [];
    const rows = entries.map(a => {
      let when = a.ts || "";
      try { if (a.ts) when = new Date(a.ts).toLocaleString(); } catch (e) {}
      return `
            <div class="tm-audit-row">
              <div class="tm-audit-when">${this._esc(when)}</div>
              <div class="tm-audit-user">${this._esc(a.user_name || a.user_id || "—")}</div>
              <div class="tm-audit-action">${this._esc(a.action || "")}</div>
              <div class="tm-audit-target">${this._esc(a.target || "")}</div>
            </div>`;
    }).join("");
    return `
        <div class="tm-section">
          <div class="tm-section-head">
            <div><h3>${this._t("panel.tab_audit")}</h3><p class="tm-meta">${this._t("panel.audit_hint")}</p></div>
            ${entries.length ? `<button type="button" class="tm-btn" data-act="audit-clear"><ha-icon icon="mdi:trash-can-outline"></ha-icon> ${this._t("panel.audit_clear")}</button>` : ""}
          </div>
          <div class="tm-section-body">
            ${entries.length ? `
              <div class="tm-audit-row tm-audit-head">
                <div>${this._t("panel.audit_when")}</div><div>${this._t("panel.audit_user")}</div>
                <div>${this._t("panel.audit_action")}</div><div>${this._t("panel.audit_target")}</div>
              </div>
              ${rows}
            ` : `<div class="tm-meta" style="padding: 8px 0;">${this._t("panel.audit_empty")}</div>`}
          </div>
        </div>`;
  }

  // -- Settings tab ------------------------------------------------------
  _renderSettingsTab() {
    const s = this._state.settings || {};
    const notifyServices = Object.keys((this._hass && this._hass.services && this._hass.services.notify) || {});
    const notifyOptions = [
      { v: "", l: this._t("panel.settings_notify_none") },
      ...notifyServices.map(k => ({ v: `notify.${k}`, l: `notify.${k}` })),
    ];
    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.settings_title")}</h2>
      </div>
      <div class="tm-settings">
        <div class="tm-section">
          <div class="tm-section-head">
            <div>
              <h3>${this._t("panel.settings_currency_title")}</h3>
              <p class="tm-meta">${this._t("panel.settings_currency_hint")}</p>
            </div>
          </div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_points_name_label")}<small>${this._t("panel.settings_points_name_hint")}</small></div>
              <input type="text" class="tm-input" data-setting="points_name" value="${this._esc(s.points_name || this._t("panel.settings_points_default"))}" placeholder="${this._esc(this._t("panel.settings_points_default"))}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_points_icon_label")}<small>${this._t("panel.settings_points_icon_hint")}</small></div>
              <ha-icon-picker data-setting="points_icon" data-current="${this._esc(s.points_icon || 'mdi:star')}"></ha-icon-picker>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_card_design_label")}<small>${this._t("panel.settings_card_design_hint")}</small></div>
              <select class="tm-select" data-setting="card_design">
                ${["classic","playroom","console","cleanpro"].map(v => `<option value="${v}" ${v === (s.card_design || "classic") ? "selected" : ""}>${this._esc(this._t("common.design." + v))}</option>`).join("")}
              </select>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_quick_points_label")}<small>${this._t("panel.settings_quick_points_hint")}</small></div>
              <input type="text" class="tm-input" data-setting="quick_point_amounts" value="${this._esc(s.quick_point_amounts == null ? "" : s.quick_point_amounts)}" placeholder="5, 10, 20">
            </div>
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div>
            <h3>${this._t("panel.settings_parents_title")}</h3>
            <p class="tm-meta">${this._t("panel.settings_parents_hint")}</p>
          </div></div>
          <div class="tm-section-body">
            ${(this._haUsers || []).filter(u => !u.is_admin).map(u => `
              <label class="tm-setting-row tm-role-row">
                <div class="tm-setting-label">${this._esc(u.name)}</div>
                <input type="checkbox" class="tm-role-check" data-parent-user="${this._esc(u.id)}" ${(s.parent_user_ids || []).includes(u.id) ? "checked" : ""}>
              </label>`).join("")
              || `<p class="tm-meta">${this._t("panel.settings_parents_empty")}</p>`}
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.settings_history_title")}</h3></div></div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_retention_label")}<small>${this._t("panel.settings_retention_hint")}</small></div>
              <input type="number" class="tm-input" min="30" max="365" data-setting="history_days" value="${s.history_days || 90}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_streak_reset_label")}<small>${this._t("panel.settings_streak_reset_hint")}</small></div>
              <select class="tm-select" data-setting="streak_reset_mode">
                ${STREAK_MODES.map(m => `<option value="${m.v}" ${m.v === (s.streak_reset_mode || "reset") ? "selected" : ""}>${this._esc(this._t(m.lk))}</option>`).join("")}
              </select>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_streak_all_chores_label")}<small>${this._t("panel.settings_streak_all_chores_hint")}</small></div>
              <ha-switch data-setting="streak_requires_all_chores" ${s.streak_requires_all_chores ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_weekend_multiplier_label")}<small>${this._t("panel.settings_weekend_multiplier_hint")}</small></div>
              <input type="number" class="tm-input" step="0.1" min="1" max="5" data-setting="weekend_multiplier" value="${s.weekend_multiplier || 1.0}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_difficulty_multipliers_label")}<small>${this._t("panel.settings_difficulty_multipliers_hint")}</small></div>
              <div class="tm-difficulty-mults">
                <label>${this._t("panel.difficulty_easy")}<input type="number" class="tm-input" step="0.1" min="0" max="10" data-setting="difficulty_multiplier_easy" value="${s.difficulty_multiplier_easy ?? 0.5}"></label>
                <label>${this._t("panel.difficulty_medium")}<input type="number" class="tm-input" step="0.1" min="0" max="10" data-setting="difficulty_multiplier_medium" value="${s.difficulty_multiplier_medium ?? 1.0}"></label>
                <label>${this._t("panel.difficulty_hard")}<input type="number" class="tm-input" step="0.1" min="0" max="10" data-setting="difficulty_multiplier_hard" value="${s.difficulty_multiplier_hard ?? 2.0}"></label>
              </div>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_calendar_projection_label")}<small>${this._t("panel.settings_calendar_projection_hint")}</small></div>
              <input type="number" class="tm-input" min="1" max="90" data-setting="calendar_projection_days" value="${s.calendar_projection_days || 14}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_surprise_enabled_label")}<small>${this._t("panel.settings_surprise_enabled_hint")}</small></div>
              <ha-switch data-setting="surprise_bonus_enabled" ${s.surprise_bonus_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_surprise_params_label")}<small>${this._t("panel.settings_surprise_params_hint")}</small></div>
              <div class="tm-difficulty-mults">
                <label>${this._t("panel.settings_surprise_chance")}<input type="number" class="tm-input" step="1" min="0" max="100" data-setting="surprise_bonus_chance" value="${s.surprise_bonus_chance ?? 15}"></label>
                <label>${this._t("panel.settings_surprise_min")}<input type="number" class="tm-input" step="1" min="0" data-setting="surprise_bonus_min" value="${s.surprise_bonus_min ?? 5}"></label>
                <label>${this._t("panel.settings_surprise_max")}<input type="number" class="tm-input" step="1" min="0" data-setting="surprise_bonus_max" value="${s.surprise_bonus_max ?? 20}"></label>
              </div>
            </div>
            <div class="tm-setting-row tm-setting-stack">
              <div class="tm-setting-label">${this._t("panel.settings_unlock_allowlist")}<small>${this._t("panel.settings_unlock_allowlist_hint")}</small></div>
              <div class="tm-allowlist">
                ${(s.unlock_allowlist || []).map(e => `
                  <span class="tm-allow-chip">
                    ${this._esc(e)}
                    <button type="button" data-act="remove-allowlist" data-id="${this._esc(e)}" title="${this._t("panel.tooltip_remove")}">✕</button>
                  </span>
                `).join("") || `<span class="tm-field-hint">${this._t("panel.settings_unlock_allowlist_empty")}</span>`}
              </div>
              <div class="tm-field-row" style="margin-top:8px">
                ${this._entityPickerField("", "_allowlist_pick", "",
                  ["switch", "light", "media_player", "input_boolean", "automation", "script", "fan", "climate"],
                  this._t("panel.settings_unlock_allowlist_add"))}
              </div>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_roulette_enabled")}<small>${this._t("panel.settings_roulette_enabled_hint")}</small></div>
              <ha-switch data-setting="roulette_enabled" ${s.roulette_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_roulette_section")}</div>
              <div class="tm-difficulty-mults">
                <label>${this._t("panel.settings_roulette_multiplier")}<input type="number" class="tm-input" step="0.5" min="1" max="5" data-setting="roulette_multiplier" value="${s.roulette_multiplier ?? 2}"></label>
                <label>${this._t("panel.settings_roulette_spins")}<input type="number" class="tm-input" step="1" min="1" max="10" data-setting="roulette_daily_spins" value="${s.roulette_daily_spins ?? 1}"></label>
              </div>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_decay_enabled_label")}<small>${this._t("panel.settings_decay_enabled_hint")}</small></div>
              <ha-switch data-setting="points_decay_enabled" ${s.points_decay_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_decay_params_label")}<small>${this._t("panel.settings_decay_params_hint")}</small></div>
              <div class="tm-difficulty-mults">
                <label>${this._t("panel.settings_decay_percent")}<input type="number" class="tm-input" step="1" min="0" max="100" data-setting="points_decay_percent" value="${s.points_decay_percent ?? 10}"></label>
                <label>${this._t("panel.settings_decay_period")}
                  <select class="tm-select" data-setting="points_decay_period">
                    <option value="weekly" ${s.points_decay_period === "weekly" ? "selected" : ""}>${this._t("panel.reward_restock_weekly")}</option>
                    <option value="monthly" ${(s.points_decay_period || "monthly") === "monthly" ? "selected" : ""}>${this._t("panel.reward_restock_monthly")}</option>
                  </select>
                </label>
              </div>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_allow_negative_label")}<small>${this._t("panel.settings_allow_negative_hint")}</small></div>
              <ha-switch data-setting="allow_negative_balance" ${s.allow_negative_balance ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_level_step_label")}<small>${this._t("panel.settings_level_step_hint")}</small></div>
              <input type="number" class="tm-input" min="1" data-setting="level_xp_step" value="${s.level_xp_step || 100}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_spendcap_enabled_label")}<small>${this._t("panel.settings_spendcap_enabled_hint")}</small></div>
              <ha-switch data-setting="spend_cap_enabled" ${s.spend_cap_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_spendcap_params_label")}<small>${this._t("panel.settings_spendcap_params_hint")}</small></div>
              <div class="tm-difficulty-mults">
                <label>${this._t("panel.settings_spendcap_amount")}<input type="number" class="tm-input" step="1" min="0" data-setting="spend_cap_amount" value="${s.spend_cap_amount ?? 0}"></label>
                <label>${this._t("panel.settings_decay_period")}
                  <select class="tm-select" data-setting="spend_cap_period">
                    <option value="weekly" ${s.spend_cap_period === "weekly" ? "selected" : ""}>${this._t("panel.reward_restock_weekly")}</option>
                    <option value="monthly" ${(s.spend_cap_period || "weekly") === "monthly" ? "selected" : ""}>${this._t("panel.reward_restock_monthly")}</option>
                  </select>
                </label>
              </div>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_interest_enabled_label")}<small>${this._t("panel.settings_interest_enabled_hint")}</small></div>
              <ha-switch data-setting="interest_enabled" ${s.interest_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_interest_params_label")}<small>${this._t("panel.settings_interest_params_hint")}</small></div>
              <div class="tm-difficulty-mults">
                <label>${this._t("panel.settings_interest_percent")}<input type="number" class="tm-input" step="1" min="0" max="100" data-setting="interest_percent" value="${s.interest_percent ?? 5}"></label>
                <label>${this._t("panel.settings_decay_period")}
                  <select class="tm-select" data-setting="interest_period">
                    <option value="weekly" ${(s.interest_period || "weekly") === "weekly" ? "selected" : ""}>${this._t("panel.reward_restock_weekly")}</option>
                    <option value="monthly" ${s.interest_period === "monthly" ? "selected" : ""}>${this._t("panel.reward_restock_monthly")}</option>
                  </select>
                </label>
              </div>
            </div>
          </div>
        </div>

        ${this._renderTimePeriodsSection()}

        ${this._renderVacationSection()}

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.settings_bonuses_title")}</h3></div></div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_streak_milestones_label")}<small>${this._t("panel.settings_streak_milestones_hint")}</small></div>
              <ha-switch data-setting="streak_milestones_enabled" ${s.streak_milestones_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_perfect_week_label")}<small>${this._t("panel.settings_perfect_week_hint")}</small></div>
              <ha-switch data-setting="perfect_week_enabled" ${s.perfect_week_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_perfect_week_all_chores_label")}<small>${this._t("panel.settings_perfect_week_all_chores_hint")}</small></div>
              <ha-switch data-setting="perfect_week_requires_all_chores" ${s.perfect_week_requires_all_chores ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_perfect_week_bonus_label")}<small>${this._t("panel.settings_perfect_week_bonus_hint")}</small></div>
              <input type="number" class="tm-input" min="0" data-setting="perfect_week_bonus" value="${s.perfect_week_bonus || 50}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_celebration_notify_label")}<small>${this._t("panel.settings_celebration_notify_hint")}</small></div>
              <ha-switch data-setting="celebration_notify" ${s.celebration_notify ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_celebration_tier_label")}<small>${this._t("panel.settings_celebration_tier_hint")}</small></div>
              <select class="tm-select" data-setting="celebration_notify_min_tier">
                <option value="1" ${String(s.celebration_notify_min_tier ?? 2) === "1" ? "selected" : ""}>${this._t("panel.settings_celebration_tier_1")}</option>
                <option value="2" ${String(s.celebration_notify_min_tier ?? 2) === "2" ? "selected" : ""}>${this._t("panel.settings_celebration_tier_2")}</option>
                <option value="3" ${String(s.celebration_notify_min_tier ?? 2) === "3" ? "selected" : ""}>${this._t("panel.settings_celebration_tier_3")}</option>
              </select>
            </div>
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.settings_avatars_title")}</h3></div></div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_avatars_label")}<small>${this._t("panel.settings_avatars_hint", {count: (this._state.avatar_catalog || []).length})}</small></div>
              <button type="button" class="tm-btn" data-act="manage-avatars">${this._t("panel.settings_avatars_btn")}</button>
            </div>
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.settings_chore_rotation_title")}</h3></div></div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_skip_confirm_label")}<small>${this._t("panel.settings_skip_confirm_hint")}</small></div>
              <ha-switch data-setting="skip_confirmation_enabled" ${s.skip_confirmation_enabled !== false ? "checked" : ""}></ha-switch>
            </div>
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.settings_notifications_title")}</h3></div></div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_notify_label")}<small>${this._t("panel.settings_notify_hint")}</small></div>
              <select class="tm-select" data-setting="notify_service">
                ${notifyOptions.map(o => `<option value="${this._esc(o.v)}" ${o.v === (s.notify_service || "") ? "selected" : ""}>${this._esc(o.l)}</option>`).join("")}
              </select>
            </div>
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.settings_show_ids_label")}</h3><p class="tm-meta">${this._t("panel.settings_show_ids_hint")}</p></div></div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.settings_show_ids_label")}</div>
              <ha-switch ${this._showIds ? "checked" : ""} data-local="show-ids"></ha-switch>
            </div>
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.backup_title")}</h3><p class="tm-meta">${this._t("panel.backup_hint")}</p></div></div>
          <div class="tm-section-body">
            <div class="tm-period-actions">
              <button type="button" class="tm-btn" data-act="config-export"><ha-icon icon="mdi:download"></ha-icon> ${this._t("panel.backup_export")}</button>
              <button type="button" class="tm-btn" data-act="config-import"><ha-icon icon="mdi:upload"></ha-icon> ${this._t("panel.backup_import")}</button>
              <input type="file" accept="application/json,.json" data-role="config-import-file" style="display:none">
            </div>
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.family_goal_title")}</h3><p class="tm-meta">${this._t("panel.family_goal_hint")}</p></div></div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.family_goal_enabled")}</div>
              <ha-switch data-setting="family_goal_enabled" ${s.family_goal_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.family_goal_name")}</div>
              <input type="text" class="tm-input" maxlength="120" data-setting="family_goal_name" value="${this._esc(s.family_goal_name || "")}" placeholder="${this._esc(this._t("panel.family_goal_name_ph"))}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.family_goal_target")}<small>${this._t("panel.family_goal_target_hint")}</small></div>
              <input type="number" class="tm-input" min="1" max="10000000" data-setting="family_goal_target" value="${this._esc(String(s.family_goal_target || 500))}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.family_goal_reward")}</div>
              <input type="text" class="tm-input" maxlength="200" data-setting="family_goal_reward" value="${this._esc(s.family_goal_reward || "")}" placeholder="${this._esc(this._t("panel.family_goal_reward_ph"))}">
            </div>
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.allowance_title")}</h3><p class="tm-meta">${this._t("panel.allowance_hint")}</p></div></div>
          <div class="tm-section-body">
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.allowance_enabled")}</div>
              <ha-switch data-setting="allowance_enabled" ${s.allowance_enabled ? "checked" : ""}></ha-switch>
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.allowance_rate")}<small>${this._t("panel.allowance_rate_hint")}</small></div>
              <input type="number" class="tm-input" min="1" max="100000" data-setting="allowance_rate" value="${this._esc(String(s.allowance_rate || 10))}">
            </div>
            <div class="tm-setting-row">
              <div class="tm-setting-label">${this._t("panel.allowance_currency")}</div>
              <input type="text" class="tm-input" maxlength="8" data-setting="allowance_currency" value="${this._esc(s.allowance_currency || "")}" placeholder="£">
            </div>
            ${(this._state.allowance_payouts || []).length ? `
              <h4 style="margin:12px 0 4px">${this._t("panel.allowance_ledger")}</h4>
              <div class="tm-table-wrap"><table class="tm-table"><tbody>
                ${(this._state.allowance_payouts || []).slice(0, 10).map(p => `
                  <tr>
                    <td>${this._esc((p.date || "").slice(0, 10))}</td>
                    <td>${this._esc(p.child_name || "")}</td>
                    <td style="text-align:right">${p.points} → ${this._esc(p.currency || "")}${this._esc(String(p.amount))}</td>
                  </tr>
                `).join("")}
              </tbody></table></div>
            ` : ""}
          </div>
        </div>

        <div class="tm-section">
          <div class="tm-section-head"><div><h3>${this._t("panel.ics_title")}</h3><p class="tm-meta">${this._t("panel.ics_hint")}</p></div></div>
          <div class="tm-section-body">
            ${this._icsUrl ? `
              <input type="text" readonly value="${this._esc(this._icsUrl)}" data-role="ics-url" style="width:100%;font-family:monospace;font-size:12px;padding:6px;margin-bottom:8px" onclick="this.select()">
              <div class="tm-period-actions">
                <button type="button" class="tm-btn" data-act="ics-copy"><ha-icon icon="mdi:content-copy"></ha-icon> ${this._t("panel.ics_copy")}</button>
                <button type="button" class="tm-btn" data-act="ics-regenerate"><ha-icon icon="mdi:refresh"></ha-icon> ${this._t("panel.ics_regenerate")}</button>
              </div>
            ` : `
              <div class="tm-period-actions">
                <button type="button" class="tm-btn" data-act="ics-show"><ha-icon icon="mdi:calendar-export"></ha-icon> ${this._t("panel.ics_show")}</button>
              </div>
            `}
          </div>
        </div>

        <div class="tm-settings-foot">
          <button type="button" class="tm-btn tm-btn-raised" data-act="save-settings">${this._t("panel.btn_save_settings")}</button>
        </div>
      </div>
    `;
  }

  // -- Notifications tab -------------------------------------------------
  _renderNotificationsTab() {
    if (!this._notifState) {
      return `<div class="tm-card tm-empty"><p>${this._t("panel.notif_loading")}</p></div>`;
    }
    const ns = this._notifState;
    const services = this._notifyServices || [];

    // Build full recipient list (children + parents) used by matrix columns + chip picker
    const recipients = [
      ...ns.recipients.children.map(c => ({ id: c.id, name: c.name, kind: "child", notify_service: c.notify_service })),
      ...ns.recipients.parents.map(p => ({ id: p.id, name: p.name, kind: "parent", notify_service: p.notify_service, enabled: p.enabled })),
    ];

    return `
      <div class="tm-toolbar">
        <h2 class="tm-toolbar-title">${this._t("panel.notif_tab_title")}</h2>
      </div>

      ${this._renderNotifRecipientsSection(ns, services)}
      ${this._renderNotifMatrixSection(ns, recipients)}
      ${this._renderNotifCustomSection(ns, recipients)}

      <div class="tm-notif-info-box">
        <strong>${this._t("panel.notif_power_user_note_title")}</strong>
        ${this._t("panel.notif_power_user_note_body")}
      </div>
    `;
  }

  _renderNotifRecipientsSection(ns, services) {
    const optTags = (selected) => {
      const opts = [`<option value=""${!selected ? " selected" : ""}>${this._t("panel.notif_none")}</option>`];
      for (const s of services) opts.push(`<option value="${this._esc(s)}"${s === selected ? " selected" : ""}>${this._esc(s)}</option>`);
      return opts.join("");
    };
    return `
      <div class="tm-card" style="margin-bottom:16px">
        <h3>${this._t("panel.notif_section_recipients")}</h3>
        <p class="tm-meta">${this._t("panel.notif_section_recipients_desc")}</p>
        <div class="tm-grid-2">
          <div>
            <h4>${this._t("panel.notif_recipients_children")}</h4>
            ${ns.recipients.children.map(c => {
              const cid = c.id.replace(/^child:/, "");
              return `
              <div class="tm-row" data-quiet-row style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:8px 0;border-top:1px solid var(--tm-border,#3a3a3a)">
                <div style="flex:1;min-width:120px"><strong>${this._esc(c.name)}</strong></div>
                <select class="tm-notif-select" data-act="notif-set-child-notify" data-child-id="${this._esc(cid)}">
                  ${optTags(c.notify_service)}
                </select>
                <div class="tm-meta" style="display:flex;align-items:center;gap:6px;width:100%" title="${this._t("panel.notif_quiet_hours_desc")}">
                  <ha-icon icon="mdi:bell-sleep" style="--mdc-icon-size:16px"></ha-icon>
                  <span>${this._t("panel.notif_quiet_hours_label")}</span>
                  <input type="time" class="tm-notif-time-input" data-act="notif-set-child-quiet" data-quiet-bound="start" data-child-id="${this._esc(cid)}" value="${this._esc(c.quiet_hours_start || "")}" style="margin:0;width:90px">
                  <span>–</span>
                  <input type="time" class="tm-notif-time-input" data-act="notif-set-child-quiet" data-quiet-bound="end" data-child-id="${this._esc(cid)}" value="${this._esc(c.quiet_hours_end || "")}" style="margin:0;width:90px">
                </div>
              </div>
            `;}).join("")}
          </div>
          <div>
            <h4>${this._t("panel.notif_recipients_parents")}</h4>
            ${ns.recipients.parents.map(p => `
              <div class="tm-row" style="display:flex;gap:10px;align-items:center;padding:8px 0;border-top:1px solid var(--tm-border,#3a3a3a)">
                <input type="text" value="${this._esc(p.name)}" data-act="notif-rename-parent" data-parent-id="${this._esc(p.id)}" style="flex:1;min-width:60px;background:transparent;border:none;color:inherit;font-size:14px;font-weight:500">
                <select class="tm-notif-select" data-act="notif-set-parent-notify" data-parent-id="${this._esc(p.id)}">
                  ${optTags(p.notify_service)}
                </select>
                <button type="button" class="tm-icon-btn" data-act="notif-delete-parent" data-parent-id="${this._esc(p.id)}" title="${this._t("panel.notif_remove_parent")}">×</button>
              </div>
            `).join("")}
            <button type="button" class="tm-btn" data-act="notif-add-parent" style="margin-top:10px;width:100%">+ ${this._t("panel.notif_add_parent")}</button>
          </div>
        </div>
      </div>
    `;
  }

  _renderNotifMatrixSection(ns, recipients) {
    return `
      <div class="tm-card" style="margin-bottom:16px">
        <h3>${this._t("panel.notif_section_matrix")}</h3>
        <p class="tm-meta">${this._t("panel.notif_section_matrix_desc")}</p>
        <div class="tm-meta" style="margin:4px 0 12px;display:flex;flex-wrap:wrap;align-items:center;gap:8px">
          <label style="font-weight:500">${this._t("panel.notif_nav_url_global_label")}</label>
          <input type="text" class="tm-input" style="flex:1;min-width:180px"
                 value="${this._esc((ns.settings && ns.settings.notification_nav_url) || "")}"
                 data-act="notif-set-nav-url-global" placeholder="/taskmate-admin">
          <div class="tm-meta" style="flex-basis:100%">${this._t("panel.notif_nav_url_global_hint")}</div>
        </div>
        <div class="tm-meta" style="margin:4px 0 12px;display:flex;flex-wrap:wrap;align-items:center;gap:8px">
          <label style="font-weight:500">${this._t("panel.notif_group_global_label")}</label>
          <input type="text" class="tm-input" style="flex:1;min-width:180px" maxlength="64"
                 value="${this._esc((ns.settings && ns.settings.notification_group) || "")}"
                 data-act="notif-set-group-global" placeholder="taskmate">
          <div class="tm-meta" style="flex-basis:100%">${this._t("panel.notif_group_global_hint")}</div>
        </div>
        <div class="tm-table-wrap">
          <table class="tm-table">
            <thead>
              <tr>
                <th style="min-width:280px">${this._t("panel.notif_matrix_col_notification")}</th>
                ${recipients.map(r => `<th style="text-align:center;min-width:80px">${this._esc(r.name)}</th>`).join("")}
              </tr>
            </thead>
            <tbody>
              ${ns.types.map(t => this._renderNotifMatrixRow(t, ns.config[t.id] || { master_enabled: false, routes: {} }, recipients, ns.settings)).join("")}
            </tbody>
          </table>
        </div>
      </div>
    `;
  }

  _renderNotifMatrixRow(t, c, recipients, settings) {
    const active = !!c.master_enabled;
    return `
      <tr class="${active ? "" : "tm-row-disabled"}">
        <td>
          <div style="display:flex;gap:10px;align-items:flex-start">
            <input type="checkbox" class="tm-notif-switch" data-act="notif-set-master" data-type-id="${this._esc(t.id)}" ${active ? "checked" : ""}>
            <div style="max-width:240px;white-space:normal">
              <strong>${this._t("notification." + t.id + ".name")}</strong>
              <div class="tm-meta">${this._t("notification." + t.id + ".description")}</div>
              ${t.id === "streak_at_risk" ? `
                <div class="tm-meta" style="margin-top:6px;display:flex;align-items:center;gap:8px">
                  ${this._t("panel.notif_streak_cutoff_label")}
                  <input type="time" class="tm-notif-time-input" value="${this._esc(settings.streak_at_risk_cutoff_time || "20:00")}" data-act="notif-set-streak-cutoff" style="display:inline;margin:0;width:90px">
                </div>
              ` : ""}
              ${t.id === "mandatory_reminder" ? `
                <div class="tm-meta" style="margin-top:6px;display:flex;align-items:center;gap:8px">
                  ${this._t("panel.notif_escalation_reminder_label")}
                  <input type="number" min="1" max="1440" class="tm-notif-time-input" value="${this._esc(String(settings.mandatory_escalation_reminder_minutes || 30))}" data-act="notif-set-escalation" data-esc-field="reminder_minutes" style="display:inline;margin:0;width:70px">
                  ${this._t("panel.notif_escalation_minutes_suffix")}
                </div>
              ` : ""}
              ${t.id === "mandatory_parent_alert" ? `
                <div class="tm-meta" style="margin-top:6px;display:flex;align-items:center;gap:8px">
                  ${this._t("panel.notif_escalation_parent_label")}
                  <input type="number" min="1" max="1440" class="tm-notif-time-input" value="${this._esc(String(settings.mandatory_escalation_parent_minutes || 120))}" data-act="notif-set-escalation" data-esc-field="parent_minutes" style="display:inline;margin:0;width:70px">
                  ${this._t("panel.notif_escalation_minutes_suffix")}
                </div>
              ` : ""}
              <div style="margin-top:6px">
                <button type="button" class="tm-btn" data-act="notif-send-test" data-type-id="${this._esc(t.id)}" style="padding:2px 10px;font-size:12px">
                  <ha-icon icon="mdi:send" style="--mdc-icon-size:14px"></ha-icon> ${this._t("panel.notif_send_test")}
                </button>
              </div>
              <div style="margin-top:6px;display:flex;align-items:center;gap:8px">
                <input type="text" class="tm-notif-time-input" style="width:150px"
                       value="${this._esc(c.nav_url || "")}"
                       data-act="notif-set-nav-url-type" data-type-id="${this._esc(t.id)}"
                       placeholder="${this._esc(this._t("panel.notif_nav_url_row_placeholder"))}">
                <input type="text" class="tm-notif-time-input" style="width:150px" maxlength="64"
                       value="${this._esc(c.group || "")}"
                       data-act="notif-set-group-type" data-type-id="${this._esc(t.id)}"
                       title="${this._esc(this._t("panel.notif_group_global_label"))}"
                       placeholder="${this._esc(this._t("panel.notif_group_row_placeholder"))}">
              </div>
            </div>
          </div>
        </td>
        ${recipients.map(r => this._renderNotifMatrixCell(t, c, r)).join("")}
      </tr>
    `;
  }

  _renderNotifMatrixCell(t, c, r) {
    const valid =
      t.audience === "both" ||
      (t.audience === "child" && r.kind === "child") ||
      (t.audience === "parent" && r.kind === "parent");
    if (!valid) return `<td class="tm-notif-matrix-cell" style="color:var(--tm-text-muted,#666)">—</td>`;
    const route = c.routes[r.id] || { enabled: false, time: null };
    return `
      <td class="tm-notif-matrix-cell">
        <input type="checkbox" class="tm-notif-switch" data-act="notif-set-route" data-type-id="${this._esc(t.id)}" data-recipient-id="${this._esc(r.id)}" data-time="${this._esc(route.time || "")}" ${route.enabled ? "checked" : ""} ${c.master_enabled ? "" : "disabled"}>
        ${t.per_recipient_time ? `
          <input type="time" class="tm-notif-time-input" data-act="notif-set-route-time" data-type-id="${this._esc(t.id)}" data-recipient-id="${this._esc(r.id)}" value="${this._esc(route.time || "20:00")}" ${(c.master_enabled && route.enabled) ? "" : "disabled"}>
        ` : ""}
      </td>
    `;
  }

  _renderNotifCustomSection(ns, recipients) {
    return `
      <div class="tm-card" style="margin-bottom:16px">
        <div style="display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;align-items:center">
          <h3 style="margin:0">${this._t("panel.notif_section_custom")}</h3>
          <button type="button" class="tm-btn tm-btn-raised" data-act="notif-add-custom">+ ${this._t("panel.notif_add_custom")}</button>
        </div>
        <p class="tm-meta">${this._t("panel.notif_section_custom_desc")}</p>
        ${(ns.custom || []).length === 0 ? `<div class="tm-empty"><p>${this._t("panel.notif_custom_empty")}</p></div>` : ""}
        ${(ns.custom || []).map(n => this._renderNotifCustomCard(n, recipients)).join("")}
      </div>
    `;
  }

  _renderNotifCustomCard(n, recipients) {
    const days = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];
    return `
      <div class="tm-notif-custom-card">
        <div class="tm-notif-custom-row">
          <input type="text" class="tm-input" value="${this._esc(n.name)}" data-act="notif-update-custom" data-custom-id="${this._esc(n.id)}" data-field="name" placeholder="${this._t("panel.notif_custom_name_placeholder")}">
          <input type="time" class="tm-input" value="${this._esc(n.time)}" data-act="notif-update-custom" data-custom-id="${this._esc(n.id)}" data-field="time">
          <input type="checkbox" class="tm-notif-switch" data-act="notif-toggle-custom" data-custom-id="${this._esc(n.id)}" ${n.enabled ? "checked" : ""}>
          <button type="button" class="tm-notif-delete" data-act="notif-delete-custom" data-custom-id="${this._esc(n.id)}" aria-label="${this._t("panel.btn_delete")}">×</button>
        </div>
        <input type="text" class="tm-input tm-notif-custom-msg" value="${this._esc(n.message_template)}" data-act="notif-update-custom" data-custom-id="${this._esc(n.id)}" data-field="message_template" placeholder="${this._t("panel.notif_custom_message_placeholder")}">
        <div class="tm-notif-toggle-group">
          ${days.map((d, i) => {
            const on = !!(n.day_mask & (1 << i));
            return `
              <label class="tm-notif-toggle tm-notif-toggle-day ${on ? "tm-notif-toggle-on" : ""}">
                <input type="checkbox" data-act="notif-toggle-day" data-custom-id="${this._esc(n.id)}" data-day="${i}" ${on ? "checked" : ""}>
                <span>${this._t("panel.notif_day_" + d.toLowerCase())}</span>
              </label>
            `;
          }).join("")}
        </div>
        <div class="tm-notif-toggle-group">
          ${recipients.map(r => {
            const on = n.recipient_ids.includes(r.id);
            const name = (r.name && r.name.trim()) || this._t("panel.notif_unnamed_recipient");
            return `
              <button type="button" class="tm-notif-toggle tm-notif-toggle-pill ${on ? "tm-notif-toggle-on" : ""}"
                      data-act="notif-toggle-recipient" data-custom-id="${this._esc(n.id)}" data-recipient-id="${this._esc(r.id)}"
                      aria-pressed="${on ? "true" : "false"}">
                ${this._esc(name)}
              </button>
            `;
          }).join("")}
        </div>
      </div>
    `;
  }

  // -- Dialogs -----------------------------------------------------------
  _openGiftDialog() {
    const children = this._state.children || [];
    if (children.length < 2) return;
    this._openDialog({ kind: "gift", data: { from: children[0].id, to: children[1].id, points: 10 } });
  }

  _renderGiftDialog() {
    const d = this._dialog.data;
    const opts = (this._state.children || []).map(c => ({ v: c.id, l: c.name }));
    return this._dialogShell(this._t("panel.gift_dialog_title"),
      [
        this._select(this._t("panel.gift_from"), "from", d.from, opts),
        this._select(this._t("panel.gift_to"), "to", d.to, opts),
        this._field(this._t("panel.gift_amount"), "points", d.points, "number"),
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-gift">${this._t("panel.gift_send")}</button>`
    );
  }

  async _doGiftPoints() {
    const d = this._dialog.data;
    const pts = Number(d.points) || 0;
    if (!d.from || !d.to || d.from === d.to) { this._showToast("err", this._t("panel.gift_err_same")); return; }
    if (pts < 1) { this._showToast("err", this._t("panel.gift_err_amount")); return; }
    const { ok, err } = await this._callWS({ type: "taskmate/gift_points", from_child_id: d.from, to_child_id: d.to, points: pts });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    this._closeDialog(true);
    await this._fetchState();
    this._showToast("ok", this._t("panel.gift_sent"));
  }

  _openAdjustDialog(childId) {
    const child = (this._state.children || []).find(c => c.id === childId);
    if (!child) return;
    this._openDialog({ kind: "adjust", data: { child_id: childId, points: 10, reason: "" } });
  }

  _renderAdjustDialog() {
    const d = this._dialog.data;
    const child = (this._state.children || []).find(c => c.id === d.child_id) || {};
    return this._dialogShell(
      this._t("panel.adjust_dialog_title", { name: child.name || this._t("panel.child_unnamed") }),
      [
        this._field(this._t("panel.adjust_amount"), "points", d.points, "number"),
        this._field(this._t("panel.adjust_reason"), "reason", d.reason, "text"),
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-danger" data-act="save-adjust-remove">${this._t("panel.adjust_remove")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-adjust-add">${this._t("panel.adjust_add")}</button>`
    );
  }

  // `sign` is +1 or -1, taken from which footer button was pressed, so the
  // dialog needs no direction control of its own.
  async _saveAdjustDialog(sign) {
    const d = this._dialog.data;
    const amount = Number(d.points);
    if (!Number.isInteger(amount) || amount < 1 || amount > 10000) {
      this._showToast("err", this._t("panel.adjust_err_amount"));
      return;
    }
    const reason = String(d.reason || "").trim() || "Admin panel adjustment";
    const ok = await this._doAdjustPoints(d.child_id, sign * amount, reason);
    if (ok) this._closeDialog(true);
  }

  // Manual point adjustment (#746). The busy key is the CHILD, not the button,
  // so a laggy tablet can neither double-award one amount nor race +5 against
  // −10 on the same balance.
  async _doAdjustPoints(childId, delta, reason = "Admin panel adjustment") {
    const amount = Math.abs(Number(delta) || 0);
    if (!childId || !Number.isInteger(amount) || amount < 1 || amount > 10000) {
      this._showToast("err", this._t("panel.adjust_err_amount"));
      return false;
    }
    if (!this._adjustBusy) this._adjustBusy = new Set();
    if (this._adjustBusy.has(childId)) return false;
    this._adjustBusy.add(childId);
    try {
      const service = delta > 0 ? "add_points" : "remove_points";
      const data = { child_id: childId, points: amount };
      if (reason) data.reason = reason;
      const { ok, err } = await this._callService(service, data);
      if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return false; }
      await this._fetchState();
      const child = (this._state.children || []).find(c => c.id === childId);
      this._showToast("ok", this._t("panel.adjust_done", {
        sign: delta > 0 ? "+" : "−",
        amount,
        points: this._state.settings.points_name || this._t("common.points"),
        child: (child && child.name) || "",
      }));
      return true;
    } finally {
      this._adjustBusy.delete(childId);
    }
  }

  _openSwapDialog(choreId) {
    const c = (this._state.chores || []).find(x => x.id === choreId);
    if (!c) return;
    const cur = c.assignment_current_child_id || "";
    const opts = (this._state.children || []).filter(ch => ch.id !== cur);
    this._openDialog({ kind: "swap", data: { chore_id: choreId, chore_name: c.name, requester: (opts[0] && opts[0].id) || "" } });
  }

  _renderSwapDialog() {
    const d = this._dialog.data;
    const c = (this._state.chores || []).find(x => x.id === d.chore_id) || {};
    const cur = c.assignment_current_child_id || "";
    const opts = (this._state.children || []).filter(ch => ch.id !== cur).map(ch => ({ v: ch.id, l: ch.name }));
    return this._dialogShell(this._t("panel.swap_dialog_title", { chore: d.chore_name }),
      this._select(this._t("panel.swap_requester"), "requester", d.requester, opts),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-swap">${this._t("panel.swap_create")}</button>`
    );
  }

  async _doRequestSwap() {
    const d = this._dialog.data;
    if (!d.requester) { this._showToast("err", this._t("panel.swap_pick_child")); return; }
    const { ok, err } = await this._callWS({ type: "taskmate/request_swap", chore_id: d.chore_id, requester_id: d.requester });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    this._closeDialog(true); await this._fetchState();
    this._showToast("ok", this._t("panel.swap_requested"));
  }

  async _doSwap(action, reqId) {
    const type = action === "approve" ? "taskmate/approve_swap" : "taskmate/reject_swap";
    const { ok, err } = await this._callWS({ type, request_id: reqId });
    if (!ok) { this._showToast("err", this._t("panel.toast_save_failed", { error: err })); return; }
    await this._fetchState();
    this._showToast("ok", this._t(action === "approve" ? "panel.swap_approved" : "panel.swap_rejected"));
  }

  _renderDialog() {
    if (this._dialog.kind === "swap")         return this._renderSwapDialog();
    if (this._dialog.kind === "gift")         return this._renderGiftDialog();
    if (this._dialog.kind === "adjust")       return this._renderAdjustDialog();
    if (this._dialog.kind === "child")        return this._renderChildDialog();
    if (this._dialog.kind === "chore")        return this._renderChoreDialog();
    if (this._dialog.kind === "reward")       return this._renderRewardDialog();
    if (this._dialog.kind === "penalty")      return this._renderPenBonDialog("penalty");
    if (this._dialog.kind === "bonus")        return this._renderPenBonDialog("bonus");
    if (this._dialog.kind === "group")        return this._renderGroupDialog();
    if (this._dialog.kind === "quest")        return this._renderQuestDialog();
    if (this._dialog.kind === "avatar-catalog") return this._renderAvatarCatalogDialog();
    if (this._dialog.kind === "challenge")    return this._renderChallengeDialog();
    if (this._dialog.kind === "apply-penalty") return this._renderApplyDialog("penalty");
    if (this._dialog.kind === "apply-bonus")   return this._renderApplyDialog("bonus");
    if (this._dialog.kind === "bulk-chore")    return this._renderBulkChoreDialog();
    if (this._dialog.kind === "reorder")       return this._renderReorderDialog();
    if (this._dialog.kind === "create-template" || this._dialog.kind === "edit-template") return this._renderCreateEditTemplateDialog();
    if (this._dialog.kind === "badge")       return this._renderBadgeDialog();
    if (this._dialog.kind === "award-badge") return this._renderAwardDialog();
    return "";
  }

  _renderCreateEditTemplateDialog() {
    const d = this._dialog.data;
    const isEdit = this._dialog.kind === "edit-template";
    return `
      <div class="tm-scrim" data-act="scrim">
        <div class="tm-dialog" style="max-width:680px">
          <div class="tm-dialog-head">
            <h2>${isEdit ? this._t("panel.template_edit_title") : this._t("panel.template_create_title")}</h2>
            <button type="button" class="tm-icon-btn" data-act="close-dialog">✕</button>
          </div>
          <div class="tm-dialog-body">
            <div class="tm-field-row" style="margin-bottom:16px">
              <div class="tm-field"><span class="tm-field-label">${this._t("panel.template_name_label")}</span><input class="tm-input" type="text" data-field="name" value="${this._esc(d.name)}"></div>
              <div class="tm-field"><span class="tm-field-label">${this._t("panel.template_icon_label")}</span><input class="tm-input" type="text" data-field="icon" value="${this._esc(d.icon)}"></div>
            </div>
            <label class="tm-field-label" style="display:block;margin-bottom:8px">${this._t("panel.template_chores_in_template")}</label>
            ${(d.chores || []).map((c, i) => `
              <div class="tm-tpl-preview-card" style="margin-bottom:8px">
                <div style="display:flex;align-items:center;gap:10px;padding:10px 14px">
                  <span style="font-weight:600;flex:1">${this._esc(c.name || this._t("panel.template_unnamed"))}</span>
                  <span class="tm-meta">${this._t("panel.pts_display", {count: c.points || 0})}</span>
                  <button type="button" class="tm-tpl-remove" data-act="tpl-dialog-remove-chore" data-idx="${i}">✕</button>
                </div>
                <div style="padding:0 14px 12px;border-top:1px solid var(--tm-border-soft)">
                  <div class="tm-field-row" style="margin-top:10px">
                    <div class="tm-field"><span class="tm-field-label">${this._t("panel.common_name")}</span><input class="tm-input" type="text" data-tpl-dialog-field="name" data-tpl-dialog-idx="${i}" value="${this._esc(c.name)}"></div>
                    <div class="tm-field"><span class="tm-field-label">${this._t("panel.common_points")}</span><input class="tm-input" type="number" data-tpl-dialog-field="points" data-tpl-dialog-idx="${i}" value="${c.points || 0}" min="0"></div>
                  </div>
                  <div class="tm-field-row">
                    <div class="tm-field"><span class="tm-field-label">${this._t("panel.chore_time_category_label")}</span>
                      <select class="tm-select" data-tpl-dialog-field="time_category" data-tpl-dialog-idx="${i}">
                        ${this._timeCategoryOptions().map(t => `<option value="${this._esc(t.v)}" ${c.time_category === t.v ? "selected" : ""}>${this._esc(t.l)}</option>`).join("")}
                      </select>
                    </div>
                    <div class="tm-field"><span class="tm-field-label">${this._t("panel.template_schedule_label")}</span>
                      <select class="tm-select" data-tpl-dialog-field="schedule_mode" data-tpl-dialog-idx="${i}">
                        ${SCHEDULE_MODES.map(s => `<option value="${s.v}" ${c.schedule_mode === s.v ? "selected" : ""}>${this._t(s.lk)}</option>`).join("")}
                      </select>
                    </div>
                  </div>
                </div>
              </div>
            `).join("")}
            <button type="button" class="tm-btn" data-act="tpl-dialog-add-chore" style="margin-top:8px">${this._t("panel.btn_add_chore_template")}</button>
          </div>
          <div class="tm-dialog-foot">
            <button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
            <button type="button" class="tm-btn tm-btn-raised" data-act="${isEdit ? "tpl-save-edited" : "tpl-save-created"}">${isEdit ? this._t("panel.btn_save_changes") : this._t("panel.btn_create_template")}</button>
          </div>
        </div>
      </div>
    `;
  }

  _renderChildDialog() {
    const d = this._dialog.data;
    const hasAvail = !!(d.availability_entity && d.availability_entity.trim());
    const hasAway = hasAvail || !!(d.unavailability_entity && d.unavailability_entity.trim());
    return this._dialogShell(this._dialog.mode === "add" ? this._t("panel.dialog_add_child") : this._t("panel.dialog_edit_child"),
      [
        this._field(this._t("panel.child_name_label"), "name", d.name, "text", this._t("panel.child_name_placeholder")),
        this._iconPickerField(this._t("panel.child_avatar_label"), "avatar", d.avatar),
        this._entityPickerField(this._t("panel.child_availability_label"), "availability_entity", d.availability_entity, ["binary_sensor", "sensor", "input_boolean", "person"],
          this._t("panel.child_availability_hint")),
        hasAvail ? this._switch(this._t("panel.child_invert_label"), "availability_inverted", d.availability_inverted,
          this._t("panel.child_invert_hint")) : "",
        this._entityPickerField(this._t("panel.child_unavailability_label"), "unavailability_entity", d.unavailability_entity, ["binary_sensor", "sensor", "input_boolean", "person", "calendar"],
          this._t("panel.child_unavailability_hint")),
        hasAway ? this._switch(this._t("panel.child_pause_streak_label"), "pause_streak_when_unavailable", d.pause_streak_when_unavailable,
          this._t("panel.child_pause_streak_hint")) : "",
        this._select(
          this._t("panel.child_link_user_label"), "linked_user_id", d.linked_user_id || "",
          [{ v: "", l: this._t("panel.child_link_user_none") }].concat(
            (this._haUsers || []).map(u => ({ v: u.id, l: u.is_admin ? `${u.name} (admin)` : u.name }))
          ),
          this._t("panel.child_link_user_hint")),
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-child">${this._t("panel.btn_save")}</button>`
    );
  }

  _renderChoreDialog() {
    const d = this._dialog.data;
    const children = this._state.children || [];
    const groups = this._state.task_groups || [];
    const memberInGroup = (d.id && groups.find(g => (g.chore_ids || []).includes(d.id))) || null;
    const showSpecificDays = d.schedule_mode === "specific_days";
    const showRecurring    = d.schedule_mode === "recurring";
    const showRotation     = ["alternating", "random", "balanced"].includes(d.assignment_mode);
    const isUnassigned     = d.assignment_mode === "unassigned";
    const calendarEntities = Object.keys((this._hass && this._hass.states) || {})
      .filter(id => id.startsWith("calendar."))
      .sort();

    const isTimedTask = d.task_type === "timed";

    return this._dialogShell(this._dialog.mode === "add" ? this._t("panel.dialog_add_chore") : this._t("panel.dialog_edit_chore"),
      [
        this._field(this._t("panel.chore_name_label"), "name", d.name, "text"),
        `<div class="tm-field-row">
          ${this._field(this._t("panel.chore_description_label"), "description", d.description, "text")}
          ${this._iconPickerField(this._t("panel.chore_icon_label"), "icon", d.icon)}
        ${this._choreImageField(d.image_url || "")}
        </div>`,
        this._select(this._t("panel.chore_task_type_label"), "task_type", d.task_type || "standard", [
          { v: "standard", l: this._t("panel.chore_task_type_standard") },
          { v: "timed", l: this._t("panel.chore_task_type_timed") },
        ], "", true),
        isTimedTask ? `<div class="tm-field-row">
          ${this._field(this._t("panel.chore_points_per_window_label"), "timed_rate_points", d.timed_rate_points || 10, "number")}
          ${this._field(this._t("panel.chore_window_minutes_label"), "timed_rate_minutes", d.timed_rate_minutes || 5, "number")}
        </div>
        <div class="tm-field-row">
          ${this._field(this._t("panel.chore_daily_cap_label"), "timed_max_daily_minutes", d.timed_max_daily_minutes || 0, "number")}
          ${this._field(this._t("panel.chore_daily_limit_label"), "daily_limit", d.daily_limit, "number")}
        </div>` : `<div class="tm-field-row">
          ${this._field(this._t("panel.chore_points_label"), "points", d.points, "number")}
          ${d.assignment_mode === "first_come" ? "" : this._field(this._t("panel.chore_daily_limit_label"), "daily_limit", d.daily_limit, "number")}
        </div>`,
        this._select(this._t("panel.chore_assignment_mode_label"), "assignment_mode", d.assignment_mode, ASSIGNMENT_MODES,
          this._t("panel.chore_assignment_mode_hint"), true),
        !isUnassigned ? (children.length > 0 ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.chore_assign_label")}</span>
            <div class="tm-chip-row">
              ${children.map(c => `
                <button type="button" class="tm-chip-btn ${(d.assigned_to || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-assigned" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
            <span class="tm-field-hint">${this._t("panel.chore_assign_hint")}</span>
          </div>
        ` : `<div class="tm-field"><span class="tm-field-hint">${this._t("panel.chore_assign_no_children")}</span></div>`) : "",
        (this._state.chores || []).filter(x => x.id !== d.id).length > 0 ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.chore_depends_label")}</span>
            <div class="tm-chip-row">
              ${(this._state.chores || []).filter(x => x.id !== d.id).map(c => `
                <button type="button" class="tm-chip-btn ${(d.depends_on || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-depends" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
            <span class="tm-field-hint">${this._t("panel.chore_depends_hint")}</span>
          </div>
        ` : "",
        showRotation && children.length > 0 ? this._select(
          this._t("panel.chore_rotation_label"), "manual_start_child_id", d.manual_start_child_id,
          [{ v: "", l: this._t("panel.chore_rotation_no_override") }, ...children.map(c => ({ v: c.id, l: c.name }))],
          this._t("panel.chore_rotation_hint")
        ) : "",
        this._select(this._t("panel.chore_time_category_label"), "time_category", d.time_category, this._timeCategoryOptions()),
        this._field(this._t("panel.chore_claim_allowance_label"), "claim_allowance_minutes", d.claim_allowance_minutes, "number",
          this._t("panel.chore_claim_allowance_hint")),
        this._select(this._t("panel.chore_schedule_mode_label"), "schedule_mode", d.schedule_mode, SCHEDULE_MODES, "", true),
        showSpecificDays ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.chore_days_label")}</span>
            <div class="tm-day-row">
              ${DAYS.map(day => `
                <button type="button" class="tm-day-btn ${(d.due_days || []).includes(day.v) ? "tm-day-on" : ""}" data-act="toggle-day" data-day="${day.v}">${this._t(day.lk)}</button>
              `).join("")}
            </div>
          </div>
        ` : "",
        showRecurring ? `
          <div class="tm-field-row">
            ${this._select(this._t("panel.chore_recurrence_label"), "recurrence", d.recurrence, RECURRENCES)}
            ${this._field(this._t("panel.chore_recurrence_day_label"), "recurrence_day", d.recurrence_day, "text", this._t("panel.chore_recurrence_day_hint"))}
          </div>
          <div class="tm-field-row">
            ${this._dateField(this._t("panel.chore_recurrence_start_label"), "recurrence_start", d.recurrence_start, this._t("panel.chore_recurrence_start_hint"))}
            ${this._select(this._t("panel.chore_first_occurrence_label"), "first_occurrence_mode", d.first_occurrence_mode, FIRST_OCCURRENCE)}
          </div>
        ` : "",
        this._select(this._t("panel.chore_completion_sound_label"), "completion_sound", d.completion_sound, COMPLETION_SOUNDS.map(s => ({ v: s, l: s }))),
        this._select(this._t("panel.chore_difficulty_label"), "difficulty", d.difficulty || "medium", [
          { v: "easy", l: this._t("panel.difficulty_easy") },
          { v: "medium", l: this._t("panel.difficulty_medium") },
          { v: "hard", l: this._t("panel.difficulty_hard") },
        ]),
        this._dateField(this._t("panel.chore_expires_label"), "expires_on", d.expires_on, this._t("panel.chore_expires_hint")),
        `<div class="tm-field-row">
          ${this._field(this._t("panel.chore_due_time_label"), "due_time", d.due_time, "time", this._t("panel.chore_due_time_hint"))}
          ${this._field(this._t("panel.chore_early_bonus_label"), "early_bonus", d.early_bonus, "number")}
          ${this._field(this._t("panel.chore_late_penalty_label"), "late_penalty", d.late_penalty, "number")}
        </div>`,
        this._switch(this._t("panel.chore_approval_label"), "requires_approval", d.requires_approval),
        this._switch(this._t("panel.chore_require_photo_label"), "require_photo", d.require_photo,
          this._t("panel.chore_require_photo_hint")),
        this._switch(this._t("panel.chore_require_availability"), "require_availability", d.require_availability,
          this._t("panel.chore_require_availability_hint")),
        this._switch(this._t("panel.chore_mandatory_label"), "mandatory", d.mandatory,
          this._t("panel.chore_mandatory_hint"), true),
        d.mandatory ? this._field(this._t("panel.chore_mandatory_penalty_label"), "mandatory_penalty_points",
          d.mandatory_penalty_points, "number", this._t("panel.chore_mandatory_penalty_hint")) : "",
        memberInGroup ? `
          <div class="tm-field">
            <span class="tm-field-hint">${this._t("panel.chore_group_hint", {name: this._esc(memberInGroup.name), policy: memberInGroup.policy})}</span>
          </div>
        ` : "",
        `<details class="tm-advanced" data-section="bonus_subtasks"${this._dialog._openAdvanced?.has("bonus_subtasks") ? " open" : ""}>
          <summary>${this._t("panel.chore_advanced_bonus_subtasks")}</summary>
          <div>
            <span class="tm-field-hint" style="margin-bottom:8px;display:block">${this._t("panel.chore_advanced_bonus_subtasks_hint")}</span>
            ${(d.bonus_subtasks || []).map((b, idx) => `
              <div class="tm-field-row" style="align-items:flex-end;gap:6px;margin-bottom:6px">
                <div class="tm-field" style="flex:2;margin:0"><input class="tm-input" placeholder="${this._t("panel.chore_subtask_name_placeholder")}" value="${this._esc(b.name)}" data-field="bonus_subtasks[${idx}].name"></div>
                <div class="tm-field" style="flex:0 0 70px;margin:0"><input class="tm-input" type="number" min="0" placeholder="${this._t("panel.chore_subtask_points_placeholder")}" value="${b.points}" data-field="bonus_subtasks[${idx}].points"></div>
                <button type="button" class="tm-btn tm-btn-icon" data-act="remove-bonus-subtask" data-idx="${idx}" title="${this._t("panel.tooltip_remove")}" style="padding:6px 10px">✕</button>
              </div>
            `).join("")}
            <button type="button" class="tm-btn" data-act="add-bonus-subtask" style="margin-top:4px">${this._t("panel.btn_add_bonus_subtask")}</button>
          </div>
        </details>`,
        `<details class="tm-advanced" data-section="visibility"${this._dialog._openAdvanced?.has("visibility") ? " open" : ""}>
          <summary>${this._t("panel.chore_advanced_visibility")}</summary>
          <div>
            ${this._entityPickerField(this._t("panel.chore_vis_entity_label"), "visibility_entity", d.visibility_entity, ["binary_sensor", "sensor", "switch", "input_boolean", "input_select"],
              this._t("panel.chore_vis_entity_hint"))}
            <div class="tm-field-row">
              ${this._select(this._t("panel.chore_vis_operator_label"), "visibility_operator", d.visibility_operator, VISIBILITY_OPS)}
              ${this._field(this._t("panel.chore_vis_value_label"), "visibility_state", d.visibility_state, "text", this._t("panel.chore_vis_value_hint"))}
            </div>
            <div class="tm-field">
              <span class="tm-field-label">${this._t("panel.chore_calendar_label")}</span>
              ${calendarEntities.length === 0 ? `<span class="tm-field-hint">${this._t("panel.chore_calendar_no_entities")}</span>` : `
                <div class="tm-chip-row">
                  ${calendarEntities.map(cid => `
                    <button type="button" class="tm-chip-btn ${(d.publish_calendar_entities || []).includes(cid) ? "tm-chip-on" : ""}" data-act="toggle-calendar" data-id="${this._esc(cid)}">
                      ${this._esc(cid)}
                    </button>
                  `).join("")}
                </div>
                <span class="tm-field-hint">${this._t("panel.chore_calendar_hint")}</span>
              `}
            </div>
            ${this._switch(this._t("panel.chore_enabled_label"), "enabled", d.enabled !== false)}
          </div>
        </details>`,
        this._dialog.mode === "edit" ? this._renderScheduledChanges(d) : "",
        `<details class="tm-advanced" data-section="weather"${this._dialog._openAdvanced?.has("weather") ? " open" : ""}>
          <summary>${this._t("panel.chore_advanced_weather")}</summary>
          <div>
            <span class="tm-field-hint" style="margin-bottom:8px;display:block">${this._t("panel.chore_weather_intro")}</span>
            ${this._entityPickerField(this._t("panel.chore_weather_entity_label"), "weather_entity", d.weather_entity, ["weather"],
              this._t("panel.chore_weather_entity_hint"))}
            ${d.weather_entity ? `
              <div class="tm-field">
                <span class="tm-field-label">${this._t("panel.chore_weather_conditions_label")}</span>
                <div class="tm-chip-row">
                  ${WEATHER_CONDITIONS.map(w => `
                    <button type="button" class="tm-chip-btn ${(d.weather_block_conditions || []).includes(w.v) ? "tm-chip-on" : ""}" data-act="toggle-weather-condition" data-id="${w.v}">
                      <ha-icon icon="${w.icon}" style="--mdc-icon-size:16px;margin-right:4px"></ha-icon>${this._t("weather.condition_" + w.v.replace(/-/g, "_"))}
                    </button>
                  `).join("")}
                </div>
                <span class="tm-field-hint">${this._t("panel.chore_weather_conditions_hint")}</span>
              </div>
              <div class="tm-field-row">
                ${this._field(this._t("panel.chore_weather_temp_min_label"), "weather_temp_min", d.weather_temp_min, "number", this._t("panel.chore_weather_temp_min_hint"))}
                ${this._field(this._t("panel.chore_weather_temp_max_label"), "weather_temp_max", d.weather_temp_max, "number", this._t("panel.chore_weather_temp_max_hint"))}
              </div>
              ${this._field(this._t("panel.chore_weather_wind_max_label"), "weather_wind_max", d.weather_wind_max, "number", this._t("panel.chore_weather_wind_max_hint"))}
              <span class="tm-field-hint">${this._t("panel.chore_weather_failopen_hint")}</span>
            ` : ""}
          </div>
        </details>`,
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-chore">${this._t("panel.btn_save")}</button>`
    );
  }

  /**
   * Scheduled config changes (#675) — queue an edit for a future date.
   * Edit-only: a chore has to exist before a change can be queued against it.
   */
  _renderScheduledChanges(d) {
    const pending = (this._state.scheduled_changes || [])
      .filter(c => c.chore_id === d.id && !c.applied)
      .sort((a, b) => a.apply_on.localeCompare(b.apply_on));
    const applied = (this._state.scheduled_changes || [])
      .filter(c => c.chore_id === d.id && c.applied)
      .sort((a, b) => b.apply_on.localeCompare(a.apply_on))
      .slice(0, 5);
    const draft = this._dialog._schedDraft || {};
    const open = this._dialog._openAdvanced?.has("scheduled");
    const children = this._state.children || [];

    const describe = (changes) => Object.entries(changes || {})
      .map(([f, v]) => {
        const label = this._t("panel.sched_field_" + f);
        const value = f === "assigned_to"
          ? (v || []).map(id => this._esc((children.find(c => c.id === id) || {}).name || id)).join(", ")
              || this._t("panel.sched_value_nobody")
          : Array.isArray(v) ? v.join(", ")
          : typeof v === "boolean" ? this._t(v ? "panel.sched_value_yes" : "panel.sched_value_no")
          : this._esc(String(v));
        return `${label}: <strong>${value}</strong>`;
      }).join(" · ");

    return `<details class="tm-advanced" data-section="scheduled"${open ? " open" : ""}>
      <summary>${this._t("panel.chore_advanced_scheduled")}${pending.length ? ` <span class="tm-sched-count">${pending.length}</span>` : ""}</summary>
      <div>
        <span class="tm-field-hint" style="margin-bottom:8px;display:block">${this._t("panel.chore_scheduled_intro")}</span>
        ${pending.length ? `
          <div class="tm-sched-list">
            ${pending.map(c => `
              <div class="tm-sched-row">
                <div class="tm-sched-when">${this._esc(c.apply_on)}</div>
                <div class="tm-sched-what">${describe(c.changes)}${c.note ? `<div class="tm-sched-note">${this._esc(c.note)}</div>` : ""}</div>
                <button type="button" class="tm-btn tm-btn-icon" data-act="remove-scheduled" data-id="${this._esc(c.id)}"
                        title="${this._t("panel.tooltip_remove")}">✕</button>
              </div>
            `).join("")}
          </div>
        ` : `<span class="tm-field-hint">${this._t("panel.chore_scheduled_none")}</span>`}

        <div class="tm-sched-add">
          <div class="tm-field-row">
            ${this._dateField(this._t("panel.sched_date_label"), "_sched_apply_on", draft.apply_on || "")}
            ${this._select(this._t("panel.sched_field_label"), "_sched_field", draft.field || "points",
              SCHEDULED_FIELDS.map(f => ({ v: f.v, lk: "panel.sched_field_" + f.v })))}
          </div>
          ${this._renderScheduledValueInput(draft, children)}
          ${this._field(this._t("panel.sched_note_label"), "_sched_note", draft.note || "", "text",
            this._t("panel.sched_note_hint"))}
          <button type="button" class="tm-btn" data-act="add-scheduled">${this._t("panel.btn_add_scheduled")}</button>
        </div>

        ${applied.length ? `
          <div class="tm-sched-applied">
            <span class="tm-field-label">${this._t("panel.chore_scheduled_history")}</span>
            ${applied.map(c => `
              <div class="tm-sched-row tm-sched-done">
                <div class="tm-sched-when">${this._esc(c.apply_on)}</div>
                <div class="tm-sched-what">${describe(c.changes)}</div>
                <span class="tm-sched-tick" title="${this._t("panel.chore_scheduled_applied")}">✓</span>
              </div>
            `).join("")}
          </div>
        ` : ""}
      </div>
    </details>`;
  }

  /** The value control depends on which field is being scheduled. */
  _renderScheduledValueInput(draft, children) {
    const field = draft.field || "points";
    const spec = SCHEDULED_FIELDS.find(f => f.v === field) || SCHEDULED_FIELDS[0];
    if (spec.kind === "children") {
      const picked = draft.value_list || [];
      return `<div class="tm-field">
        <span class="tm-field-label">${this._t("panel.sched_value_label")}</span>
        <div class="tm-chip-row">
          ${children.map(c => `
            <button type="button" class="tm-chip-btn ${picked.includes(c.id) ? "tm-chip-on" : ""}"
                    data-act="toggle-scheduled-child" data-id="${this._esc(c.id)}">${this._esc(c.name)}</button>
          `).join("")}
        </div>
      </div>`;
    }
    if (spec.kind === "days") {
      const picked = draft.value_list || [];
      return `<div class="tm-field">
        <span class="tm-field-label">${this._t("panel.sched_value_label")}</span>
        <div class="tm-chip-row">
          ${DAYS.map(day => `
            <button type="button" class="tm-chip-btn ${picked.includes(day.v) ? "tm-chip-on" : ""}"
                    data-act="toggle-scheduled-day" data-id="${day.v}">${this._t(day.lk)}</button>
          `).join("")}
        </div>
      </div>`;
    }
    if (spec.kind === "bool") {
      return this._select(this._t("panel.sched_value_label"), "_sched_value", draft.value ?? "true",
        [{ v: "true", lk: "panel.sched_value_yes" }, { v: "false", lk: "panel.sched_value_no" }]);
    }
    if (spec.kind === "select") {
      return this._select(this._t("panel.sched_value_label"), "_sched_value", draft.value ?? spec.options[0].v, spec.options);
    }
    return this._field(this._t("panel.sched_value_label"), "_sched_value", draft.value ?? "",
      spec.kind === "number" ? "number" : "text");
  }

  _schedDraft() {
    if (!this._dialog._schedDraft) this._dialog._schedDraft = { field: "points", value_list: [] };
    return this._dialog._schedDraft;
  }

  _toggleScheduledListValue(value) {
    const draft = this._schedDraft();
    const list = [...(draft.value_list || [])];
    const i = list.indexOf(value);
    if (i >= 0) list.splice(i, 1); else list.push(value);
    draft.value_list = list;
    this._render();
  }

  async _saveAllowlist(list) {
    const { ok, err } = await this._callWS({ type: "taskmate/update_settings", unlock_allowlist: list });
    if (!ok) { this._showToast("err", err); return; }
    await this._fetchState();
    this._render();
  }

  _removeFromAllowlist(entityId) {
    const current = (this._state.settings || {}).unlock_allowlist || [];
    this._saveAllowlist(current.filter(e => e !== entityId));
  }

  _addToAllowlist(entityId) {
    const current = (this._state.settings || {}).unlock_allowlist || [];
    if (!entityId || current.includes(entityId)) return;
    this._saveAllowlist([...current, entityId]);
  }

  async _addScheduledChange() {
    const d = this._dialog.data;
    const draft = this._schedDraft();
    const spec = SCHEDULED_FIELDS.find(f => f.v === (draft.field || "points")) || SCHEDULED_FIELDS[0];
    if (!draft.apply_on) { this._showToast("err", this._t("panel.toast_sched_date_required")); return; }

    let value;
    if (spec.kind === "children" || spec.kind === "days") {
      value = draft.value_list || [];
    } else if (spec.kind === "bool") {
      value = (draft.value ?? "true") === "true";
    } else if (spec.kind === "number") {
      if (draft.value === "" || draft.value == null) { this._showToast("err", this._t("panel.toast_sched_value_required")); return; }
      value = Number(draft.value);
    } else {
      value = draft.value ?? "";
    }

    const { ok, err } = await this._callWS({
      type: "taskmate/scheduled/add",
      chore_id: d.id,
      apply_on: draft.apply_on,
      changes: { [spec.v]: value },
      note: draft.note || "",
    });
    if (!ok) { this._showToast("err", err); return; }
    this._dialog._schedDraft = { field: spec.v, value_list: [] };
    this._showToast("ok", this._t("panel.toast_sched_added"));
    await this._fetchState();
    this._render();
  }

  async _removeScheduledChange(changeId) {
    const { ok, err } = await this._callWS({ type: "taskmate/scheduled/remove", change_id: changeId });
    if (!ok) { this._showToast("err", err); return; }
    this._showToast("ok", this._t("panel.toast_sched_removed"));
    await this._fetchState();
    this._render();
  }

  _renderRewardDialog() {
    const d = this._dialog.data;
    const children = this._state.children || [];
    const pointsName = this._state.settings.points_name || this._t("common.points");
    return this._dialogShell(this._dialog.mode === "add" ? this._t("panel.dialog_add_reward") : this._t("panel.dialog_edit_reward"),
      [
        this._field(this._t("panel.chore_name_label"), "name", d.name, "text"),
        this._field(this._t("panel.chore_description_label"), "description", d.description, "text"),
        `<div class="tm-field-row">
          ${this._field(this._t("panel.reward_cost_label", {points_name: pointsName}), "cost", d.cost, "number")}
          ${this._iconPickerField(this._t("panel.reward_icon_label"), "icon", d.icon)}
        </div>`,
        children.length > 0 ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.reward_assigned_label")}</span>
            <div class="tm-chip-row">
              ${children.map(c => `
                <button type="button" class="tm-chip-btn ${(d.assigned_to || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-reward-assigned" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
            <span class="tm-field-hint">${this._t("panel.reward_assigned_hint")}</span>
          </div>
        ` : "",
        // Jackpot first; toggling it re-renders so the Pool switch below can be
        // hidden — jackpots are always pool-mode (#552), so the separate Pool
        // toggle would be redundant and confusing.
        this._switch(this._t("panel.reward_is_jackpot_label"), "is_jackpot", d.is_jackpot,
          this._t("panel.reward_is_jackpot_hint"), true),
        d.is_jackpot ? "" : this._switch(this._t("panel.reward_pool_label"), "pool_enabled", d.pool_enabled,
          this._t("panel.reward_pool_hint")),
        `<div class="tm-field-row">
          ${this._field(this._t("panel.reward_quantity_label"), "quantity_str", d.quantity_str, "text", this._t("panel.reward_quantity_hint"))}
          ${this._dateField(this._t("panel.reward_expires_label"), "expires_at", d.expires_at, this._t("panel.reward_expires_hint"))}
        </div>`,
        this._switch(this._t("panel.reward_restock_label"), "restock_enabled", d.restock_enabled,
          this._t("panel.reward_restock_hint")),
        `<div class="tm-field-row">
          ${this._field(this._t("panel.reward_restock_amount"), "restock_amount", d.restock_amount, "number")}
          ${this._select(this._t("panel.reward_restock_period"), "restock_period", d.restock_period || "weekly", [
            { v: "daily", l: this._t("panel.reward_restock_daily") },
            { v: "weekly", l: this._t("panel.reward_restock_weekly") },
            { v: "monthly", l: this._t("panel.reward_restock_monthly") },
          ])}
        </div>`,
        this._renderRewardUnlock(d),
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-reward">${this._t("panel.btn_save")}</button>`
    );
  }

  /**
   * Timed unlock (#678). Only offers entities the parent has allowlisted in
   * Settings — a reward must not be able to reach arbitrary devices.
   */
  _renderRewardUnlock(d) {
    const allow = (this._state.settings || {}).unlock_allowlist || [];
    const open = this._dialog._openAdvanced?.has("unlock");
    if (!allow.length) {
      return `<details class="tm-advanced" data-section="unlock"${open ? " open" : ""}>
        <summary>${this._t("panel.reward_unlock_section")}</summary>
        <div><span class="tm-field-hint">${this._t("panel.reward_unlock_no_allowlist")}</span></div>
      </details>`;
    }
    const options = [{ v: "", lk: "panel.reward_unlock_none" }]
      .concat(allow.map(e => ({ v: e, l: e })));
    return `<details class="tm-advanced" data-section="unlock"${open ? " open" : ""}>
      <summary>${this._t("panel.reward_unlock_section")}</summary>
      <div>
        <span class="tm-field-hint" style="margin-bottom:8px;display:block">${this._t("panel.reward_unlock_intro")}</span>
        ${this._select(this._t("panel.reward_unlock_entity"), "unlock_entity", d.unlock_entity || "", options)}
        ${d.unlock_entity ? this._field(this._t("panel.reward_unlock_minutes"), "unlock_minutes",
          d.unlock_minutes ?? 30, "number", this._t("panel.reward_unlock_minutes_hint")) : ""}
      </div>
    </details>`;
  }

  _renderAvatarCatalogDialog() {
    const rows = (this._dialog.data.catalog || []);
    const typeOpts = [
      { v: "free",   l: this._t("panel.avatar_unlock_free") },
      { v: "level",  l: this._t("panel.avatar_unlock_level") },
      { v: "points", l: this._t("panel.avatar_unlock_points") },
      { v: "streak", l: this._t("panel.avatar_unlock_streak") },
    ];
    const rowHtml = rows.map((a, i) => `
      <div class="tm-avatar-row">
        <div class="tm-avatar-row-icon">${this._mdi(a.icon || "mdi:account-circle")}</div>
        <div class="tm-avatar-row-fields">
          <input class="tm-input" type="text" data-field="catalog[${i}].label" value="${this._esc(a.label || "")}" placeholder="${this._t("panel.avatar_label_ph")}">
          <input class="tm-input" type="text" data-field="catalog[${i}].icon" value="${this._esc(a.icon || "")}" placeholder="mdi:rocket-launch">
          <select class="tm-select" data-field="catalog[${i}].unlock_type">
            ${typeOpts.map(o => `<option value="${o.v}" ${(a.unlock_type || "free") === o.v ? "selected" : ""}>${this._esc(o.l)}</option>`).join("")}
          </select>
          <input class="tm-input" type="number" min="0" data-field="catalog[${i}].unlock_value" value="${Number(a.unlock_value) || 0}" ${(a.unlock_type || "free") === "free" ? "disabled" : ""}>
        </div>
        <button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="avatar-del-row" data-idx="${i}">✕</button>
      </div>
    `).join("");
    return this._dialogShell(this._t("panel.avatar_dialog_title"),
      [
        `<p class="tm-field-hint">${this._t("panel.avatar_dialog_hint")}</p>`,
        rows.length ? `<div class="tm-avatar-rows">${rowHtml}</div>` : `<div class="tm-meta">${this._t("panel.avatar_empty")}</div>`,
        `<button type="button" class="tm-btn tm-btn-sm" data-act="avatar-add-row" style="margin-top:10px;">＋ ${this._t("panel.avatar_add_row")}</button>`,
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-avatar-catalog">${this._t("panel.btn_save")}</button>`
    );
  }

  _renderQuestDialog() {
    const d = this._dialog.data;
    const children = this._state.children || [];
    const pointsName = this._state.settings.points_name || this._t("common.points");
    const chores = this._state.chores || [];
    const choreById = {};
    chores.forEach(c => { choreById[c.id] = c; });
    const steps = d.steps || [];
    // Ordered step list with move/remove controls
    const stepRows = steps.map((sid, i) => `
      <div class="tm-quest-step-row">
        <span class="tm-quest-step-num">${i + 1}</span>
        <span class="tm-quest-step-name">${this._esc(choreById[sid] ? choreById[sid].name : this._t("panel.quest_missing_chore"))}</span>
        <button type="button" class="tm-btn tm-btn-sm" data-act="quest-step-move" data-idx="${i}" data-dir="up" ${i === 0 ? "disabled" : ""}>▲</button>
        <button type="button" class="tm-btn tm-btn-sm" data-act="quest-step-move" data-idx="${i}" data-dir="down" ${i === steps.length - 1 ? "disabled" : ""}>▼</button>
        <button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="toggle-quest-step" data-id="${this._esc(sid)}">✕</button>
      </div>
    `).join("");
    return this._dialogShell(this._dialog.mode === "add" ? this._t("panel.dialog_add_quest") : this._t("panel.dialog_edit_quest"),
      [
        this._field(this._t("panel.chore_name_label"), "name", d.name, "text"),
        this._field(this._t("panel.chore_description_label"), "description", d.description, "text"),
        `<div class="tm-field-row">
          ${this._field(this._t("panel.quest_bonus_label", {points_name: pointsName}), "bonus_points", d.bonus_points, "number")}
          ${this._iconPickerField(this._t("panel.reward_icon_label"), "icon", d.icon)}
        </div>`,
        `<div class="tm-field">
          <span class="tm-field-label">${this._t("panel.quest_steps_label")}</span>
          <span class="tm-field-hint">${this._t("panel.quest_steps_hint")}</span>
          ${steps.length ? `<div class="tm-quest-step-list">${stepRows}</div>` : `<div class="tm-meta">${this._t("panel.quest_no_steps")}</div>`}
          <div class="tm-chip-row" style="margin-top:8px;">
            ${chores.map(c => `
              <button type="button" class="tm-chip-btn ${steps.includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-quest-step" data-id="${this._esc(c.id)}">
                ${this._esc(c.name)}
              </button>
            `).join("")}
          </div>
        </div>`,
        children.length > 0 ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.quest_assigned_label")}</span>
            <div class="tm-chip-row">
              ${children.map(c => `
                <button type="button" class="tm-chip-btn ${(d.assigned_to || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-quest-assigned" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
            <span class="tm-field-hint">${this._t("panel.quest_assigned_hint")}</span>
          </div>
        ` : "",
        this._switch(this._t("panel.quest_repeatable_label"), "repeatable", d.repeatable,
          this._t("panel.quest_repeatable_hint")),
        this._switch(this._t("panel.quest_active_label"), "active", d.active !== false,
          this._t("panel.quest_active_hint")),
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-quest">${this._t("panel.btn_save")}</button>`
    );
  }

  _renderPenBonDialog(kind) {
    const d = this._dialog.data;
    const children = this._state.children || [];
    const isPenalty = kind === "penalty";
    return this._dialogShell(this._dialog.mode === "add" ? (isPenalty ? this._t("panel.dialog_add_penalty") : this._t("panel.dialog_add_bonus")) : (isPenalty ? this._t("panel.dialog_edit_penalty") : this._t("panel.dialog_edit_bonus")),
      [
        this._field(this._t("panel.chore_name_label"), "name", d.name, "text"),
        this._field(this._t("panel.chore_description_label"), "description", d.description, "text"),
        `<div class="tm-field-row">
          ${this._field(this._t(isPenalty ? "panel.penbon_points_deduct" : "panel.penbon_points_award"), "points", d.points, "number")}
          ${this._iconPickerField(this._t("panel.reward_icon_label"), "icon", d.icon)}
        </div>`,
        children.length > 0 ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.penbon_applies_to")}</span>
            <div class="tm-chip-row">
              ${children.map(c => `
                <button type="button" class="tm-chip-btn ${(d.assigned_to || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-penbon-assigned" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
            <span class="tm-field-hint">${this._t("panel.penbon_applies_to_hint")}</span>
          </div>
        ` : "",
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-${kind}">${this._t("panel.btn_save")}</button>`
    );
  }

  _renderApplyDialog(kind) {
    const d = this._dialog.data;
    const item = d.item;
    const children = this._state.children || [];
    const isPenalty = kind === "penalty";
    const eligible = (item.assigned_to || []).length === 0
      ? children
      : children.filter(c => (item.assigned_to || []).includes(c.id));
    return this._dialogShell(isPenalty ? this._t("panel.dialog_apply_penalty", {name: item.name}) : this._t("panel.dialog_apply_bonus", {name: item.name}),
      `<p>${isPenalty ? this._t("panel.penalty_apply_text", {points: item.points}) : this._t("panel.bonus_apply_text", {points: item.points})}</p>
       ${eligible.length === 0 ? `<p class="tm-meta">${this._t("panel.penbon_no_eligible", {kind})}</p>` : `
        <div class="tm-chip-row" style="margin-top: 12px;">
          ${eligible.map(c => `
            <button type="button" class="tm-btn" data-act="do-apply-${kind}" data-id="${this._esc(item.id)}" data-child="${this._esc(c.id)}">
              ${this._esc(c.name)}
            </button>
          `).join("")}
        </div>
       `}`,
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_close")}</button>`
    );
  }

  _renderGroupDialog() {
    const d = this._dialog.data;
    const chores = (this._state.chores || []).filter(c => c.assignment_mode && !["everyone", "first_come"].includes(c.assignment_mode));
    return this._dialogShell(this._dialog.mode === "add" ? this._t("panel.dialog_add_group") : this._t("panel.dialog_edit_group"),
      [
        this._field(this._t("panel.common_name"), "name", d.name, "text"),
        this._select(this._t("panel.group_policy_label"), "policy", d.policy, TASK_GROUP_POLICIES,
          this._t("panel.group_policy_hint")),
        chores.length === 0 ? `
          <div class="tm-field">
            <span class="tm-field-hint">${this._t("panel.group_no_rotation_chores")}</span>
          </div>
        ` : `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.group_chores_label")}</span>
            <div class="tm-chip-row">
              ${chores.map(c => `
                <button type="button" class="tm-chip-btn ${(d.chore_ids || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-group-chore" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
          </div>
        `,
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-group">${this._t("panel.btn_save")}</button>`
    );
  }

  _renderBulkChoreDialog() {
    const d = this._dialog.data;
    const children = this._state.children || [];
    return this._dialogShell(this._t("panel.bulk_title"),
      [
        `<div class="tm-field">
          <span class="tm-field-label">${this._t("panel.bulk_chore_names_label")}</span>
          <textarea class="tm-textarea" data-field="chore_names" rows="6" placeholder="${this._t("panel.bulk_chore_names_placeholder")}">${this._esc(d.chore_names || "")}</textarea>
          <span class="tm-field-hint">${this._t("panel.bulk_chore_names_hint")}</span>
        </div>`,
        `<div class="tm-field-row">
          ${this._field(this._t("panel.bulk_points_label"), "points", d.points, "number")}
          ${this._field(this._t("panel.chore_daily_limit_label"), "daily_limit", d.daily_limit, "number")}
        </div>`,
        children.length > 0 ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.bulk_assigned_to")}</span>
            <div class="tm-chip-row">
              ${children.map(c => `
                <button type="button" class="tm-chip-btn ${(d.assigned_to || []).includes(c.id) ? "tm-chip-on" : ""}" data-act="toggle-assigned" data-id="${this._esc(c.id)}">
                  ${this._esc(c.name)}
                </button>
              `).join("")}
            </div>
            <span class="tm-field-hint">${this._t("panel.bulk_leave_empty_hint")}</span>
          </div>
        ` : "",
        this._select(this._t("panel.chore_time_category_label"), "time_category", d.time_category, this._timeCategoryOptions()),
        this._select(this._t("panel.chore_schedule_mode_label"), "schedule_mode", d.schedule_mode, SCHEDULE_MODES, "", true),
        d.schedule_mode === "specific_days" ? `
          <div class="tm-field">
            <span class="tm-field-label">${this._t("panel.bulk_days_label")}</span>
            <div class="tm-day-row">
              ${DAYS.map(day => `
                <button type="button" class="tm-day-btn ${(d.due_days || []).includes(day.v) ? "tm-day-on" : ""}" data-act="toggle-bulk-day" data-day="${day.v}">${this._t(day.lk)}</button>
              `).join("")}
            </div>
          </div>
        ` : "",
        this._select(this._t("panel.chore_completion_sound_label"), "completion_sound", d.completion_sound, COMPLETION_SOUNDS.map(s => ({ v: s, l: s }))),
        this._select(this._t("panel.chore_difficulty_label"), "difficulty", d.difficulty || "medium", [
          { v: "easy", l: this._t("panel.difficulty_easy") },
          { v: "medium", l: this._t("panel.difficulty_medium") },
          { v: "hard", l: this._t("panel.difficulty_hard") },
        ]),
        this._dateField(this._t("panel.chore_expires_label"), "expires_on", d.expires_on, this._t("panel.chore_expires_hint")),
        `<div class="tm-field-row">
          ${this._field(this._t("panel.chore_due_time_label"), "due_time", d.due_time, "time", this._t("panel.chore_due_time_hint"))}
          ${this._field(this._t("panel.chore_early_bonus_label"), "early_bonus", d.early_bonus, "number")}
          ${this._field(this._t("panel.chore_late_penalty_label"), "late_penalty", d.late_penalty, "number")}
        </div>`,
        this._switch(this._t("panel.chore_approval_label"), "requires_approval", d.requires_approval),
        this._switch(this._t("panel.chore_require_photo_label"), "require_photo", d.require_photo,
          this._t("panel.chore_require_photo_hint")),
      ].join(""),
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-bulk-chores">${this._t("panel.btn_create_chores")}</button>`
    );
  }

  _renderReorderDialog() {
    const d = this._dialog.data;
    const choreById = Object.fromEntries((this._state.chores || []).map(c => [c.id, c]));
    const title = d.global ? this._t("panel.reorder_global_title") : this._t("panel.reorder_title", {name: d.name});
    const hint = d.global ? this._t("panel.reorder_global_hint") : this._t("panel.reorder_hint");
    return this._dialogShell(title,
      `<p class="tm-meta">${hint}</p>
       <div class="tm-reorder-list">
         ${(d.order || []).map((id, idx, arr) => {
           const c = choreById[id];
           if (!c) return "";
           return `
             <div class="tm-reorder-item" draggable="true" data-drag-id="${this._esc(id)}">
               <div class="tm-reorder-handle">⠿</div>
               <div class="tm-reorder-name">${this._esc(c.name)}</div>
               <div class="tm-reorder-points tm-numeric">${c.points}</div>
               <div class="tm-reorder-moves">
                 <button type="button" class="tm-reorder-move" data-act="reorder-move" data-dir="-1" data-drag-id="${this._esc(id)}" ${idx === 0 ? "disabled" : ""} title="${this._t("reorder.move_up")}" aria-label="${this._t("reorder.move_up")}">▲</button>
                 <button type="button" class="tm-reorder-move" data-act="reorder-move" data-dir="1" data-drag-id="${this._esc(id)}" ${idx === arr.length - 1 ? "disabled" : ""} title="${this._t("reorder.move_down")}" aria-label="${this._t("reorder.move_down")}">▼</button>
               </div>
             </div>
           `;
         }).join("")}
       </div>`,
      `<button type="button" class="tm-btn" data-act="close-dialog">${this._t("panel.btn_cancel")}</button>
       <button type="button" class="tm-btn tm-btn-raised" data-act="save-chore-order">${this._t("panel.btn_save_order")}</button>`
    );
  }

  // ---- form helpers ----------------------------------------------------
  _dialogShell(title, body, footer) {
    return `
      <div class="tm-scrim" data-act="scrim">
        <div class="tm-dialog">
          <header class="tm-dialog-head">
            <h2>${this._esc(title)}</h2>
            <button type="button" class="tm-icon-btn" data-act="close-dialog" title="${this._t("panel.tooltip_close")}">&times;</button>
          </header>
          <div class="tm-dialog-body">${body}</div>
          <footer class="tm-dialog-foot">${footer}</footer>
        </div>
      </div>`;
  }

  _field(label, name, value, type = "text", hint = "") {
    return `
      <div class="tm-field">
        <span class="tm-field-label">${this._esc(label)}</span>
        <input
          class="tm-input"
          data-field="${name}"
          value="${this._esc(value == null ? "" : value)}"
          ${type === "number" ? 'type="number"' : 'type="text"'}
        >
        ${hint ? `<span class="tm-field-hint">${hint}</span>` : ""}
      </div>`;
  }

  _dateField(label, name, value, hint = "") {
    return `
      <div class="tm-field">
        <span class="tm-field-label">${this._esc(label)}</span>
        <input
          class="tm-input"
          data-field="${name}"
          value="${this._esc(value || "")}"
          type="date"
        >
        ${hint ? `<span class="tm-field-hint">${hint}</span>` : ""}
      </div>`;
  }

  _select(label, name, value, options, hint = "", rerender = false) {
    return `
      <div class="tm-field">
        <span class="tm-field-label">${this._esc(label)}</span>
        <select
          class="tm-select"
          data-field="${name}"
          ${rerender ? 'data-rerender="true"' : ""}
        >
          ${options.map(o => `<option value="${this._esc(o.v)}" ${String(o.v) === String(value) ? "selected" : ""}>${this._esc(o.lk ? this._t(o.lk) : o.l)}</option>`).join("")}
        </select>
        ${hint ? `<span class="tm-field-hint">${hint}</span>` : ""}
      </div>`;
  }

  _switch(label, name, checked, hint = "", rerender = false) {
    return `
      <div class="tm-check-row">
        <ha-switch data-field="${name}" ${checked ? "checked" : ""} ${rerender ? 'data-rerender="true"' : ""}></ha-switch>
        <div>
          <div class="tm-check-title">${this._esc(label)}</div>
          ${hint ? `<span class="tm-field-hint">${hint}</span>` : ""}
        </div>
      </div>`;
  }

  /** The chore picture well (#750): thumbnail or empty state, Upload, Remove. */
  _choreImageField(value) {
    const busy = this._choreImageBusy;
    return `
      <div class="tm-field">
        <span class="tm-field-label">${this._t("panel.chore_image_label")}</span>
        <div class="tm-image-well">
          ${value
            ? `<img class="tm-image-thumb" src="${this._esc(value)}" alt="">`
            : `<div class="tm-image-empty">${this._t("panel.chore_image_empty")}</div>`}
          <div class="tm-image-actions">
            <button type="button" class="tm-btn tm-btn-sm" data-act="chore-image-pick" ${busy ? "disabled" : ""}>
              ${busy ? this._t("panel.chore_image_uploading") : this._t("panel.chore_image_upload")}
            </button>
            ${value ? `<button type="button" class="tm-btn tm-btn-sm tm-btn-danger" data-act="chore-image-remove">${this._t("panel.chore_image_remove")}</button>` : ""}
          </div>
        </div>
        <span class="tm-field-hint">${this._t("panel.chore_image_hint")}</span>
      </div>`;
  }

  _iconPickerField(label, name, value) {
    return `
      <div class="tm-field">
        <span class="tm-field-label">${this._esc(label)}</span>
        <ha-icon-picker data-field="${name}" data-current="${this._esc(value || "")}"></ha-icon-picker>
      </div>`;
  }

  _entityDomainIcon(entityId) {
    const domain = (entityId || "").split(".")[0];
    const map = {
      binary_sensor: "mdi:checkbox-marked-circle-outline",
      sensor: "mdi:eye",
      input_boolean: "mdi:toggle-switch",
      person: "mdi:account",
      calendar: "mdi:calendar",
      switch: "mdi:light-switch",
      input_select: "mdi:form-dropdown",
    };
    return map[domain] || "mdi:puzzle";
  }

  // Route an entity-picker change to the open dialog, or to the settings-page
  // entity draft when no dialog is open (settings entity pickers).
  _setEntityField(field, value) {
    if (this._dialog?.data) { this._dialog.data[field] = value; }
    else { (this._settingsEntityDraft = this._settingsEntityDraft || {})[field] = value; }
  }

  _entityPickerField(label, name, value, domains, hint = "") {
    const friendly = value && this._hass?.states?.[value]?.attributes?.friendly_name || "";
    const icon = value ? (this._hass?.states?.[value]?.attributes?.icon || this._entityDomainIcon(value)) : "";
    return `
      <div class="tm-field">
        <span class="tm-field-label">${this._esc(label)}</span>
        <div class="tm-ep" data-ep-field="${name}" data-ep-domains="${(domains || []).join(",")}">
          ${value
            ? `<div class="tm-ep-selected">
                <ha-icon icon="${this._esc(icon)}" class="tm-ep-selected-icon"></ha-icon>
                <span class="tm-ep-selected-text">${this._esc(friendly || value)}</span>
                <button type="button" class="tm-entity-clear" data-act="clear-field" data-field="${name}" title="${this._t("panel.tooltip_clear")}">✕</button>
              </div>`
            : `<div class="tm-ep-search-wrap">
                <ha-icon icon="mdi:magnify" class="tm-ep-search-icon"></ha-icon>
                <input type="text" class="tm-ep-input" placeholder="${this._t("panel.entity_search_placeholder")}" value="" autocomplete="off">
              </div>`
          }
        </div>
        ${hint ? `<span class="tm-field-hint">${hint}</span>` : ""}
      </div>`;
  }

  _openEntityDropdown(input) {
    this._closeEntityDropdown();
    const picker = input.closest(".tm-ep");
    if (!picker || !this._hass?.states) return;
    const field = picker.dataset.epField;
    const domains = (picker.dataset.epDomains || "").split(",").filter(Boolean);
    const query = input.value.toLowerCase();
    const entities = Object.entries(this._hass.states)
      .filter(([id, st]) => {
        if (domains.length && !domains.some(d => id.startsWith(d + "."))) return false;
        if (!query) return true;
        const fn = st.attributes?.friendly_name || "";
        return id.toLowerCase().includes(query) || fn.toLowerCase().includes(query);
      })
      .sort(([a], [b]) => a.localeCompare(b))
      .slice(0, 50);
    if (!entities.length) return;
    const dd = document.createElement("div");
    dd.className = "tm-ep-dropdown";
    entities.forEach(([id, st]) => {
      const fn = st.attributes?.friendly_name || "";
      const stateIcon = st.attributes?.icon || this._entityDomainIcon(id);
      const opt = document.createElement("div");
      opt.className = "tm-ep-option";
      opt.dataset.act = "pick-entity";
      opt.dataset.field = field;
      opt.dataset.value = id;
      const iconEl = document.createElement("ha-icon");
      iconEl.setAttribute("icon", stateIcon);
      iconEl.className = "tm-ep-option-icon";
      opt.appendChild(iconEl);
      const textWrap = document.createElement("div");
      textWrap.className = "tm-ep-option-text";
      const idDiv = document.createElement("div");
      idDiv.className = "tm-ep-id";
      idDiv.textContent = fn || id;
      textWrap.appendChild(idDiv);
      if (fn) {
        const nameDiv = document.createElement("div");
        nameDiv.className = "tm-ep-name";
        nameDiv.textContent = id;
        textWrap.appendChild(nameDiv);
      }
      opt.appendChild(textWrap);
      dd.appendChild(opt);
    });
    picker.appendChild(dd);
  }

  _closeEntityDropdown() {
    const dd = this.querySelector(".tm-ep-dropdown");
    if (dd) dd.remove();
  }

  // ---- chore row action menu (kebab) -----------------------------------
  _toggleRowMenu(btn) {
    const id = btn.dataset.id;
    const wasOpen = this._rowMenuEl && this._rowMenuForId === id;
    this._closeRowMenu();
    if (!wasOpen) this._openRowMenu(btn, id);
  }

  _openRowMenu(btn, id) {
    const c = (this._state.chores || []).find(x => x.id === id);
    if (!c) return;
    const esc = this._esc.bind(this);
    const items = [];
    if (this._state.parent_completable && this._state.parent_completable[id])
      items.push({ act: "parent-complete-chore", icon: "👤✓", label: this._t("panel.parent_complete_tooltip") });
    if (["alternating", "random", "balanced"].includes(c.assignment_mode))
      items.push({ act: "skip-chore", icon: "⏭", label: this._t("panel.btn_skip_chore") });
    if (["alternating", "random", "balanced", "first_come"].includes(c.assignment_mode))
      items.push({ act: "request-swap", icon: "⇄", label: this._t("panel.swap_btn") });
    items.push({ act: "toggle-chore-active", icon: c.enabled === false ? "▶" : "⏸", label: c.enabled === false ? this._t("panel.btn_activate_chore") : this._t("panel.btn_deactivate_chore") });
    items.push({ act: "clone-chore", icon: "⧉", label: this._t("panel.btn_clone_chore") });
    items.push({ act: "delete-chore", icon: "🗑", label: this._t("panel.btn_delete"), danger: true });

    const menu = document.createElement("div");
    menu.className = "tm-row-menu";
    menu.setAttribute("role", "menu");
    menu.style.visibility = "hidden";
    menu.innerHTML = items.map(it =>
      `<button type="button" role="menuitem" class="tm-row-menu-item${it.danger ? " tm-row-menu-danger" : ""}" data-act="${it.act}" data-id="${esc(id)}">
         <span class="tm-row-menu-icon">${it.icon}</span><span>${esc(it.label)}</span>
       </button>`).join("");
    this.appendChild(menu);

    // Position fixed at the kebab, right-aligned, flipping up / clamping to viewport.
    const rect = btn.getBoundingClientRect();
    const mw = menu.offsetWidth, mh = menu.offsetHeight, gap = 4, pad = 8;
    let left = rect.right - mw;
    if (left < pad) left = pad;
    if (left + mw > window.innerWidth - pad) left = window.innerWidth - pad - mw;
    let top = rect.bottom + gap;
    if (top + mh > window.innerHeight - pad) top = Math.max(pad, rect.top - gap - mh);
    menu.style.left = `${Math.round(left)}px`;
    menu.style.top = `${Math.round(top)}px`;
    menu.style.visibility = "visible";

    this._rowMenuEl = menu;
    this._rowMenuForId = id;
    btn.setAttribute("aria-expanded", "true");
    // Detach if the page scrolls or resizes under the (fixed) menu.
    this._onRowMenuDismiss = () => this._closeRowMenu();
    window.addEventListener("resize", this._onRowMenuDismiss, true);
    window.addEventListener("scroll", this._onRowMenuDismiss, true);
  }

  _closeRowMenu() {
    if (this._onRowMenuDismiss) {
      window.removeEventListener("resize", this._onRowMenuDismiss, true);
      window.removeEventListener("scroll", this._onRowMenuDismiss, true);
      this._onRowMenuDismiss = null;
    }
    if (this._rowMenuEl) { this._rowMenuEl.remove(); this._rowMenuEl = null; }
    if (this._rowMenuForId) {
      const btn = this.querySelector(`[data-act="toggle-row-menu"][data-id="${CSS.escape(this._rowMenuForId)}"]`);
      if (btn) btn.setAttribute("aria-expanded", "false");
      this._rowMenuForId = null;
    }
  }

  _emptyState(icon, title, copy, action, label) {
    return `
      <div class="tm-empty">
        <div class="tm-empty-icon">${icon}</div>
        <h3>${this._esc(title)}</h3>
        <p>${this._esc(copy)}</p>
        <button type="button" class="tm-btn tm-btn-raised" data-act="${action}">${this._esc(label)}</button>
      </div>
    `;
  }

  _idBadge(id) {
    if (!this._showIds || !id) return "";
    return `<span class="tm-id-badge"><code>${this._esc(id)}</code><button type="button" class="tm-id-copy" data-act="copy-id" data-id="${this._esc(id)}" title="${this._t("panel.toast_copied")}"><ha-icon icon="mdi:content-copy"></ha-icon></button></span>`;
  }

  _labelOf(arr, val) {
    const item = arr.find(x => x.v === val);
    if (!item) return val || "";
    return item.lk ? this._t(item.lk) : (item.l || val || "");
  }

  _esc(str) {
    return String(str == null ? "" : str)
      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
  }

  // Optional numeric field: "" clears the limit (sent as null). 0 is a real
  // threshold — a 0 °C minimum is meaningful — so it can't stand in for "unset".
  _optNum(value) {
    if (value === "" || value === null || value === undefined) return null;
    const n = Number(value);
    return Number.isFinite(n) ? n : null;
  }

  // Security defense-in-depth: only ever emit our own auth-gated photo endpoint
  // into an href/src. _esc() escapes HTML metacharacters but does NOT neutralize
  // a javascript:/external scheme, so guard the URL itself. Signed URLs keep the
  // /api/taskmate/photo/ prefix (the ?authSig= is appended), so they still pass.
  _safePhotoUrl(u) {
    return typeof u === "string" && u.startsWith("/api/taskmate/photo/") ? u : "";
  }

  _fmtNum(n) {
    if (n == null) return "0";
    return new Intl.NumberFormat().format(n);
  }

  // Quick point-adjust amounts (#746). Stored as a comma-separated string like
  // "5, 10, 20" — same shape as streak_milestones — so the generic settings
  // collector can round-trip it as a plain text input. Junk degrades to the
  // default rather than rendering a broken button row.
  _quickPointAmounts() {
    const raw = (this._state.settings || {}).quick_point_amounts;
    const parsed = String(raw == null ? "" : raw)
      .split(",")
      .map(p => Number(p.trim()))
      .filter(n => Number.isInteger(n) && n >= 1 && n <= 10000);
    const unique = [...new Set(parsed)].slice(0, 3);
    return unique.length ? unique : [5, 10, 20];
  }

  _mdi(name) {
    return `<ha-icon icon="${this._esc(name || "mdi:account-circle")}"></ha-icon>`;
  }

  _styles() {
    return `<style>
      /* Fill the panel area without depending on a percentage-height chain.
         HA nests us as taskmate-panel → ha-panel-custom → partial-panel-resolver
         → ha-drawer, and the middle two are display:inline / height:auto. A
         height:100% here only resolves while some ancestor happens to carry a
         definite height; where it doesn't, the shell collapses to its content
         height and the nav column visibly stops partway down every short page
         (#754). A flex column of exactly 100dvh is immune to the ancestor
         chain: panel_custom gives the element the whole viewport — HA draws no
         toolbar above it — so 100dvh is the full panel area, not an overflow.
         It must be height, not min-height: the shell scrolls its own body,
         and a floor-only rule would let a long section (Settings) grow the
         panel past the viewport and scroll the nav column off the page. */
      taskmate-panel {
        display: flex; flex-direction: column; height: 100dvh;

        --tm-bg:            var(--primary-background-color, #fafafa);
        --tm-surface-0:     var(--card-background-color, #fff);
        --tm-surface-1:     var(--card-background-color, #fff);
        --tm-surface-2:     var(--secondary-background-color, #f5f5f5);
        --tm-surface-3:     var(--secondary-background-color, #e8e8e8);
        --tm-surface-hover: var(--secondary-background-color, #f5f5f5);

        --tm-border:        var(--divider-color, #e0e0e0);
        --tm-border-strong: var(--divider-color, #d4d4d4);
        --tm-border-soft:   var(--divider-color, #e8e8e8);

        --tm-text:          var(--primary-text-color, #212121);
        --tm-text-muted:    var(--secondary-text-color, #727272);
        --tm-text-faint:    var(--secondary-text-color, #727272);
        --tm-text-vfaint:   var(--disabled-text-color, #bdbdbd);

        --tm-accent:        var(--primary-color, #03a9f4);
        --tm-accent-hover:  color-mix(in srgb, var(--primary-color, #03a9f4), #000 14%);
        --tm-accent-press:  var(--primary-color, #03a9f4);
        --tm-accent-soft:   color-mix(in srgb, var(--primary-color, #03a9f4), transparent 90%);
        --tm-accent-border: color-mix(in srgb, var(--primary-color, #03a9f4), transparent 70%);
        --tm-accent-text:   var(--primary-color, #03a9f4);
        --tm-accent-glow:   color-mix(in srgb, var(--primary-color, #03a9f4), transparent 85%);

        --tm-positive:      var(--success-color, #4caf50);
        --tm-positive-soft: color-mix(in srgb, var(--success-color, #4caf50), transparent 90%);
        --tm-positive-border:color-mix(in srgb, var(--success-color, #4caf50), transparent 70%);
        --tm-warning:       var(--warning-color, #ff9800);
        --tm-warning-soft:  color-mix(in srgb, var(--warning-color, #ff9800), transparent 90%);
        --tm-warning-border:color-mix(in srgb, var(--warning-color, #ff9800), transparent 70%);
        --tm-danger:        var(--error-color, #db4437);
        --tm-danger-soft:   color-mix(in srgb, var(--error-color, #db4437), transparent 90%);
        --tm-danger-border: color-mix(in srgb, var(--error-color, #db4437), transparent 70%);

        --tm-gold:          var(--warning-color, #ff9800);
        --tm-gold-soft:     color-mix(in srgb, var(--warning-color, #ff9800), transparent 90%);
        --tm-pool:          var(--accent-color, #ff9800);
        --tm-pool-soft:     color-mix(in srgb, var(--accent-color, #ff9800), transparent 90%);
        --tm-sticky:        var(--info-color, #039be5);
        --tm-sticky-soft:   color-mix(in srgb, var(--info-color, #039be5), transparent 90%);

        --tm-radius-sm:     8px;
        --tm-radius:        var(--ha-card-border-radius, 12px);
        --tm-radius-lg:     var(--ha-card-border-radius, 12px);

        --tm-shadow-xs:     none;
        --tm-shadow-sm:     none;
        --tm-shadow:        var(--ha-card-box-shadow, none);
        --tm-shadow-lg:     var(--ha-card-box-shadow, 0 2px 6px rgba(0,0,0,0.15));
        --tm-shadow-focus:  0 0 0 2px var(--tm-accent-glow);
        --tm-easing:        ease;

        --tm-sidebar-w:     240px;

        background: var(--tm-bg);
        color: var(--tm-text);
        font-family: var(--paper-font-body1_-_font-family, Roboto, "Noto Sans", sans-serif);
        font-size: 14px;
        line-height: 1.5;
        -webkit-font-smoothing: antialiased;
      }

      /* ===== Shell ===== */
      .tm-shell {
        display: grid;
        grid-template-columns: var(--tm-sidebar-w) 1fr;
        /* Flex to fill the panel rather than height:100% — see the note on
           taskmate-panel above (#754). min-height:0 lets the grid shrink
           instead of being forced to its content height. */
        flex: 1 1 auto;
        min-height: 0;
        position: relative;
        overflow: hidden;
      }
      .tm-main { display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
      .tm-shell > [data-zone], .tm-main > [data-zone] { display: contents; }

      /* ===== Sidebar ===== */
      .tm-sidebar {
        background: var(--tm-surface-0);
        border-right: 1px solid var(--tm-border);
        display: flex; flex-direction: column;
        overflow: hidden;
      }
      .tm-brand {
        display: flex; align-items: center; gap: 10px;
        padding: 14px 16px;
        border-bottom: 1px solid var(--tm-border-soft);
      }
      .tm-brand-mark {
        width: 28px; height: 28px;
        border-radius: 7px;
        background: var(--tm-accent);
        color: var(--text-primary-color, #fff);
        display: grid; place-items: center;
        flex-shrink: 0;
      }
      .tm-brand-mark ha-icon { --mdc-icon-size: 16px; }
      .tm-brand-text {
        flex: 1; min-width: 0;
        font-weight: 600; font-size: 14px;
        letter-spacing: -0.01em;
        line-height: 1.15;
      }
      .tm-brand-text small {
        display: block;
        font-weight: 400;
        color: var(--tm-text-faint);
        font-size: 11px;
        font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
        margin-top: 1px;
      }
      .tm-brand-wiki {
        flex-shrink: 0;
        width: 28px; height: 28px;
        border-radius: 7px;
        display: grid; place-items: center;
        color: var(--tm-text-faint);
        text-decoration: none;
        transition: color 0.15s ease, background 0.15s ease;
      }
      .tm-brand-wiki ha-icon { --mdc-icon-size: 18px; }
      .tm-brand-wiki:hover, .tm-brand-wiki:focus-visible {
        color: var(--tm-accent);
        background: var(--tm-border-soft);
        outline: none;
      }
      .tm-nav {
        padding: 10px 8px;
        flex: 1;
        overflow-y: auto;
      }
      .tm-nav-group { margin-bottom: 16px; }
      .tm-nav-group:last-child { margin-bottom: 0; }
      .tm-nav-head {
        font-size: 10.5px;
        font-weight: 600;
        color: var(--tm-text-faint);
        letter-spacing: 0.06em;
        text-transform: uppercase;
        padding: 4px 10px 6px;
      }
      .tm-nav-item {
        display: flex; align-items: center; gap: 10px;
        padding: 7px 10px;
        border-radius: var(--tm-radius-sm);
        color: var(--tm-text-muted);
        cursor: pointer;
        font-size: 13px;
        font-weight: 500;
        font-family: inherit;
        background: transparent; border: 0;
        width: 100%; text-align: left;
        position: relative;
        transition: all 0.1s var(--tm-easing);
      }
      .tm-nav-item:hover { background: var(--tm-surface-2); color: var(--tm-text); }
      .tm-nav-active {
        background: var(--tm-surface-2);
        color: var(--tm-text);
        font-weight: 600;
      }
      .tm-nav-active::before {
        content: ""; position: absolute;
        left: -8px; top: 6px; bottom: 6px; width: 2px;
        background: var(--tm-accent);
        border-radius: 0 2px 2px 0;
      }
      .tm-nav-icon {
        width: 16px; height: 16px;
        display: grid; place-items: center;
        color: var(--tm-text-faint);
        flex-shrink: 0;
      }
      .tm-nav-icon ha-icon { --mdc-icon-size: 16px; }
      .tm-nav-active .tm-nav-icon { color: var(--tm-accent); }
      .tm-nav-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
      .tm-nav-badge {
        font-size: 11px;
        color: var(--tm-text-faint);
        background: var(--tm-surface-3);
        padding: 1px 7px;
        border-radius: 999px;
        font-variant-numeric: tabular-nums;
        line-height: 1.4;
      }
      .tm-nav-active .tm-nav-badge {
        background: var(--tm-accent-soft);
        color: var(--tm-accent-text);
      }
      .tm-nav-badge-urgent {
        background: var(--tm-warning-soft) !important;
        color: var(--tm-warning) !important;
      }

      /* ===== Topbar ===== */
      .tm-topbar {
        height: 52px;
        display: flex; align-items: center; gap: 12px;
        padding: 0 24px;
        border-bottom: 1px solid var(--tm-border);
        background: var(--tm-surface-0);
        flex-shrink: 0;
      }
      .tm-menu-btn {
        display: none;
        background: transparent;
        border: 0;
        color: var(--tm-text);
        padding: 6px;
        margin-left: -6px;
        border-radius: 8px;
        cursor: pointer;
        align-items: center; justify-content: center;
      }
      .tm-menu-btn:hover { background: var(--tm-surface-2); }
      .tm-menu-btn ha-icon { --mdc-icon-size: 22px; }
      .tm-crumbs {
        display: flex; align-items: center; gap: 8px;
        font-size: 13px;
        color: var(--tm-text-faint);
        flex: 1;
      }
      .tm-crumbs strong {
        color: var(--tm-text);
        font-weight: 600;
        letter-spacing: -0.005em;
      }
      .tm-crumbs-sep { color: var(--tm-text-vfaint); }
      .tm-approval-pill {
        background: var(--tm-warning-soft);
        color: var(--tm-warning);
        border: 1px solid var(--tm-warning-border);
        /* 24px high is too small to hit reliably on a phone; this is the
           panel's only route to the pending-approvals queue. */
        min-height: 32px;
        box-sizing: border-box;
        padding: 4px 12px 4px 10px;
        border-radius: 999px;
        font-size: 12px; font-weight: 500;
        cursor: pointer;
        font-family: inherit;
        display: inline-flex; align-items: center; gap: 6px;
        transition: all 0.1s var(--tm-easing);
      }
      .tm-approval-pill:hover { filter: brightness(0.97); }
      .tm-approval-dot {
        width: 6px; height: 6px; border-radius: 50%;
        background: var(--tm-warning);
        animation: tm-pulse 2s ease-in-out infinite;
      }
      @keyframes tm-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } }

      /* ===== Mobile section picker (hidden by default, shown on narrow) ===== */
      .tm-mobilenav { display: none; }

      /* Approval banner */
      .tm-approval-banner {
        display: flex; align-items: center; gap: 12px;
        padding: 10px 24px;
        background: var(--tm-warning-soft);
        border-bottom: 1px solid var(--tm-warning-border);
        color: var(--tm-warning);
        font-size: 13px;
      }
      .tm-approval-banner ha-icon { --mdc-icon-size: 18px; color: var(--tm-warning); }
      .tm-approval-banner span { flex: 1; }
      .tm-approval-banner strong { color: var(--tm-text); font-weight: 600; }

      /* Body */
      .tm-body { flex: 1; overflow-y: auto; padding: 24px 32px 56px; background: var(--tm-bg); }
      .tm-body-inner { max-width: 1180px; margin: 0 auto; }

      /* Toolbar */
      .tm-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
      .tm-toolbar-title {
        margin: 0; font-size: 20px; font-weight: 600;
        letter-spacing: -0.018em;
      }
      .tm-toolbar-count {
        color: var(--tm-text-faint); font-weight: 400;
        margin-left: 6px;
        font-variant-numeric: tabular-nums;
        font-size: 16px;
      }

      /* Search input */
      .tm-search-wrap {
        position: relative;
        flex: 1; min-width: 240px; max-width: 400px;
      }
      .tm-search-icon {
        position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
        --mdc-icon-size: 14px; color: var(--tm-text-faint);
        pointer-events: none;
      }
      .tm-search {
        width: 100%;
        box-sizing: border-box;
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
        padding: 6px 10px 6px 32px;
        color: var(--tm-text);
        font-size: 13px;
        font-family: inherit;
        box-shadow: var(--tm-shadow-xs);
        transition: all 0.1s var(--tm-easing);
      }
      .tm-search:hover { border-color: var(--tm-border-strong); }
      .tm-search:focus {
        outline: 0;
        border-color: var(--tm-accent);
        box-shadow: var(--tm-shadow-focus);
      }
      .tm-search::placeholder { color: var(--tm-text-vfaint); }
      .tm-search-clear {
        position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
        background: var(--tm-surface-2);
        color: var(--tm-text-muted);
        border: 0;
        width: 20px; height: 20px;
        border-radius: 50%; cursor: pointer; font-size: 11px;
        display: grid; place-items: center;
      }
      .tm-search-clear:hover { background: var(--tm-surface-3); color: var(--tm-text); }

      /* Buttons */
      .tm-btn {
        display: inline-flex; align-items: center; justify-content: center;
        gap: 6px; padding: 8px 16px;
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
        background: var(--tm-surface-0);
        color: var(--tm-accent);
        font-size: 13px; font-weight: 500;
        font-family: inherit;
        cursor: pointer;
        transition: all 0.12s var(--tm-easing);
        white-space: nowrap;
        text-decoration: none;
        line-height: 1.3;
      }
      .tm-btn:hover {
        background: var(--tm-accent-soft);
        border-color: var(--tm-accent);
      }
      .tm-btn:disabled {
        opacity: 0.6;
        cursor: default;
        pointer-events: none;
      }
      @keyframes tm-spin { to { transform: rotate(360deg); } }
      .tm-btn-spinner {
        width: 12px; height: 12px;
        border: 2px solid currentColor;
        border-top-color: transparent;
        border-radius: 50%;
        animation: tm-spin 0.7s linear infinite;
        flex-shrink: 0;
      }
      @media (prefers-reduced-motion: reduce) {
        /* Still reads as busy — the ring just doesn't rotate. */
        .tm-btn-spinner { animation: none; opacity: 0.7; }
      }
      .tm-btn-raised {
        background: var(--tm-accent);
        color: var(--text-primary-color, #fff);
        border-color: var(--tm-accent);
        box-shadow: var(--tm-shadow-xs);
      }
      .tm-btn-raised:hover {
        background: var(--tm-accent-hover);
        border-color: var(--tm-accent-hover);
        color: var(--text-primary-color, #fff);
        box-shadow: var(--tm-shadow-sm);
      }
      .tm-btn-sm {
        padding: 5px 12px; font-size: 12.5px;
      }
      .tm-btn-danger {
        color: var(--tm-danger);
        border-color: var(--tm-danger-soft);
      }
      .tm-btn-danger:hover {
        background: var(--tm-danger-soft);
        border-color: var(--tm-danger);
      }
      .tm-icon-btn {
        width: 28px; height: 28px;
        border: 0; background: transparent;
        border-radius: var(--tm-radius-sm);
        display: grid; place-items: center;
        color: var(--tm-text-faint);
        cursor: pointer;
        font-family: inherit;
        transition: all 0.1s var(--tm-easing);
      }
      .tm-icon-btn ha-icon { --mdc-icon-size: 16px; }
      .tm-icon-btn:hover { background: var(--tm-surface-2); color: var(--tm-text); }

      /* Cards */
      .tm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 14px; }
      .tm-card {
        position: relative; overflow: hidden;
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-lg);
        padding: 18px;
        box-shadow: var(--tm-shadow-xs);
        transition: all 0.15s var(--tm-easing);
        margin-bottom: 14px;
      }
      .tm-card:last-child { margin-bottom: 0; }
      .tm-card:hover { border-color: var(--tm-border-strong); box-shadow: var(--tm-shadow-sm); }
      .tm-card-error { border-left: 3px solid var(--tm-danger); color: var(--tm-danger); }
      .tm-loading { color: var(--tm-text-muted); }

      .tm-section-title {
        margin: 0 0 12px;
        font-size: 14px; font-weight: 600;
        letter-spacing: -0.005em;
        display: flex; align-items: center; gap: 8px;
      }

      /* Child / reward / group cards share head/avatar */
      .tm-child-card { display: flex; flex-direction: column; gap: 14px; margin-bottom: 0; }
      .tm-child-head { display: flex; align-items: center; gap: 12px; }
      .tm-avatar {
        width: 44px; height: 44px;
        border-radius: 10px;
        background: var(--tm-accent-soft);
        border: 1px solid var(--tm-border);
        display: grid; place-items: center;
        flex-shrink: 0;
        color: var(--tm-accent);
      }
      .tm-avatar ha-icon { --mdc-icon-size: 24px; }
      .tm-avatar-reward  { color: var(--tm-gold); }
      .tm-child-name { min-width: 0; flex: 1; }
      .tm-child-name h3 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
      .tm-meta { color: var(--tm-text-faint); font-size: 12px; margin-top: 2px; word-break: break-word; }
      /* Entity ids are long and unbreakable. Left to wrap they split mid-token
         and tear the pill background across two lines, so truncate instead —
         the full value is on the title attribute. */
      .tm-meta code { font-family: ui-monospace, "SF Mono", Menlo, monospace; background: var(--tm-surface-2); padding: 1px 6px; border-radius: 4px; font-size: 11px; color: var(--tm-text-muted); display: inline-block; max-width: 100%; vertical-align: bottom; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
      .tm-text-muted { color: var(--tm-text-muted); }

      .tm-stats-row {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        gap: 1px;
        background: var(--tm-border-soft);
        border: 1px solid var(--tm-border-soft);
        border-radius: var(--tm-radius);
        overflow: hidden;
      }
      .tm-stat {
        background: var(--tm-surface-1);
        padding: 10px 12px;
      }
      .tm-stat-value {
        font-size: 17px; font-weight: 600;
        letter-spacing: -0.01em;
        font-variant-numeric: tabular-nums;
        color: var(--tm-text);
        line-height: 1.15;
      }
      .tm-stat-label {
        font-size: 11px;
        color: var(--tm-text-faint);
        margin-top: 2px; font-weight: 400;
        letter-spacing: 0.01em;
      }
      .tm-stat-highlight .tm-stat-value { color: var(--tm-gold); }

      /* Manual point adjustment strip on child cards (#746). Each -set is a
         nowrap group so the row folds as "minus / plus + ⋯" on a narrow card
         instead of orphaning the ⋯ button on a line of its own. */
      /* Chore picture well (#750). */
      .tm-image-well {
        display: flex; align-items: center; gap: 12px;
        padding: 10px; border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm); background: var(--tm-surface-2);
      }
      .tm-image-thumb {
        width: 56px; height: 56px; min-width: 0; min-height: 0;
        object-fit: cover; border-radius: var(--tm-radius-sm); display: block;
        border: 1px solid var(--tm-border);
      }
      .tm-image-empty {
        width: 56px; height: 56px; display: grid; place-items: center;
        border: 1px dashed var(--tm-border); border-radius: var(--tm-radius-sm);
        color: var(--tm-text-faint); font-size: 11px; text-align: center;
      }
      .tm-image-actions { display: flex; gap: 6px; flex-wrap: wrap; }

      .tm-points-adjust {
        display: flex; gap: 4px; flex-wrap: wrap;
        align-items: center;
        padding-top: 10px;
      }
      .tm-points-adjust-set { display: flex; gap: 4px; align-items: center; }
      .tm-points-adjust-gap { flex: 1 1 8px; min-width: 4px; }
      .tm-points-adjust .tm-btn { min-width: 40px; justify-content: center; padding-inline: 8px; }

      .tm-card-foot {
        display: flex; gap: 6px;
        margin-top: auto;
        padding-top: 12px;
        border-top: 1px solid var(--tm-border-soft);
        flex-wrap: wrap;
        align-items: center;
      }

      .tm-add-tile {
        background: transparent;
        border: 1.5px dashed var(--tm-border-strong);
        border-radius: var(--tm-radius-lg);
        padding: 36px 18px;
        color: var(--tm-text-faint);
        text-align: center;
        font-size: 13px; font-weight: 500;
        cursor: pointer;
        font-family: inherit;
        display: grid; place-items: center; gap: 8px;
        transition: all 0.12s var(--tm-easing);
        min-height: 200px;
      }
      .tm-add-tile:hover {
        border-color: var(--tm-accent);
        color: var(--tm-accent-text);
        background: var(--tm-accent-soft);
      }
      .tm-add-plus {
        width: 32px; height: 32px;
        border-radius: 50%;
        background: var(--tm-surface-2);
        display: grid; place-items: center;
        font-size: 18px;
        transition: all 0.15s var(--tm-easing);
      }
      .tm-add-tile:hover .tm-add-plus { background: var(--tm-accent); color: var(--text-primary-color, #fff); }

      /* Tables */
      .tm-table-wrap {
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-lg);
        /* Scroll horizontally on narrow screens so wide tables (e.g. Chores)
           stay usable and the action buttons remain reachable, instead of being
           clipped by the rounded container. */
        overflow-x: auto;
        overflow-y: hidden;
        -webkit-overflow-scrolling: touch;
        box-shadow: var(--tm-shadow-xs);
      }
      .tm-table { width: 100%; border-collapse: collapse; font-size: 13px; }
      .tm-table th, .tm-table td {
        text-align: left;
        padding: 11px 16px;
        border-bottom: 1px solid var(--tm-border-soft);
        vertical-align: middle;
        /* Keep columns at their natural width so the table overflows (and the
           wrap scrolls) on small devices rather than squashing cells. */
        white-space: nowrap;
      }
      .tm-table th {
        color: var(--tm-text-faint);
        font-weight: 500; font-size: 11.5px;
        text-transform: uppercase; letter-spacing: 0.02em;
        background: var(--tm-surface-2);
        border-bottom: 1px solid var(--tm-border);
      }
      .tm-table tr:last-child td { border-bottom: 0; }
      .tm-row { transition: background 0.1s var(--tm-easing); }
      .tm-row:hover { background: var(--tm-surface-hover); }
      .tm-row-disabled { opacity: 0.5; }
      .tm-row-icon { display: inline-flex; vertical-align: middle; margin-right: 10px; opacity: 0.7; --mdc-icon-size: 18px; color: var(--tm-text-muted); }
      .tm-row-actions { text-align: right; white-space: nowrap; }
      .tm-row-actions > div { display: inline-flex; gap: 4px; align-items: center; justify-content: flex-end; }

      /* Keep the task-name column visible when a wide table scrolls
         horizontally (issue #533). Pinned only outside bulk-select mode,
         where the checkbox column sits to the name's left. */
      .tm-table th.tm-col-sticky, .tm-table td.tm-col-sticky {
        position: sticky; left: 0; z-index: 1;
      }
      .tm-table td.tm-col-sticky { background: var(--tm-surface-0); }
      .tm-table th.tm-col-sticky { z-index: 3; }
      .tm-row:hover td.tm-col-sticky { background: var(--tm-surface-hover); }

      /* Row actions (✏ / ⋮) ride inside the pinned name cell so they stay
         reachable without scrolling the wide table right (issue #568).
         Pinned with the name outside bulk mode; scroll with it in bulk mode. */
      .tm-name-cell { display: flex; align-items: center; gap: 12px; }
      .tm-name-cell .tm-name-main { min-width: 0; }
      .tm-name-cell .tm-name-actions {
        margin-left: auto; display: inline-flex; gap: 4px;
        align-items: center; flex-shrink: 0;
      }

      /* Floating kebab (⋮) action menu for chore rows. Rendered fixed at the
         document root so the table wrap's overflow never clips it. */
      .tm-row-menu {
        position: fixed; z-index: 300;
        min-width: 188px; padding: 4px;
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius);
        box-shadow: var(--tm-shadow-lg);
        animation: tm-row-menu-in 0.1s var(--tm-easing);
      }
      @keyframes tm-row-menu-in {
        from { opacity: 0; transform: translateY(-4px); }
        to   { opacity: 1; transform: none; }
      }
      .tm-row-menu-item {
        display: flex; align-items: center; gap: 10px; width: 100%;
        padding: 8px 10px; border: 0; background: transparent;
        border-radius: var(--tm-radius-sm);
        color: var(--tm-text); font-family: inherit; font-size: 13px;
        text-align: left; cursor: pointer; white-space: nowrap;
        transition: background 0.08s var(--tm-easing);
      }
      .tm-row-menu-item:hover { background: var(--tm-surface-hover); }
      .tm-row-menu-icon { width: 18px; text-align: center; flex-shrink: 0; font-size: 13px; }
      .tm-row-menu-danger { color: var(--tm-danger); }
      .tm-row-menu-danger:hover { background: var(--tm-danger-soft); }
      .tm-numeric { font-feature-settings: "tnum"; }
      .tm-yes { color: var(--tm-positive); font-weight: 500; }
      .tm-no  { color: var(--tm-text-faint); }
      .tm-neg { color: var(--tm-danger); }
      .tm-pos { color: var(--tm-positive); }
      .tm-current-assignee { color: var(--tm-accent); font-weight: 600; }
      .tm-cost { color: var(--tm-gold); }
      .tm-table-hint { margin-top: 8px; font-size: 11px; }

      .tm-inline-input {
        background: var(--tm-surface-1);
        border: 1px solid var(--tm-accent);
        border-radius: 6px;
        padding: 4px 8px;
        color: var(--tm-text);
        font-size: 13px; font-weight: 500;
        font-family: inherit;
        max-width: 240px;
        outline: 0;
        box-shadow: 0 0 0 3px var(--tm-accent-soft);
      }

      /* Pills */
      .tm-pill {
        display: inline-flex; align-items: center; gap: 5px;
        padding: 2px 8px; border-radius: 999px;
        font-size: 11.5px; font-weight: 500;
        line-height: 1.5;
        background: var(--tm-surface-2); color: var(--tm-text-muted);
        border: 1px solid var(--tm-border);
        margin-left: 4px; vertical-align: middle;
      }
      .tm-pill-accent  { background: var(--tm-accent-soft);   color: var(--tm-accent-text); border-color: var(--tm-accent-border); }
      .tm-pill-success { background: var(--tm-positive-soft); color: var(--tm-positive);    border-color: var(--tm-positive-border); }
      .tm-pill-warn    { background: var(--tm-warning-soft);  color: var(--tm-warning);     border-color: var(--tm-warning-border); }
      .tm-pill-danger  { background: var(--tm-danger-soft);   color: var(--tm-danger);      border-color: var(--tm-danger-border); }
      .tm-pill-pool    { background: var(--tm-pool-soft);     color: var(--tm-pool);        border-color: color-mix(in srgb, var(--tm-pool), transparent 75%); }
      .tm-pill-sticky  { background: var(--tm-sticky-soft);   color: var(--tm-sticky);      border-color: color-mix(in srgb, var(--tm-sticky), transparent 75%); }
      .tm-pill-spread  { background: var(--tm-positive-soft); color: var(--tm-positive);    border-color: var(--tm-positive-border); }
      .tm-pill-jackpot { background: var(--tm-gold-soft);     color: var(--tm-gold);        border-color: color-mix(in srgb, var(--tm-gold), transparent 75%); }
      .tm-pill-muted   { background: var(--tm-surface-2);     color: var(--tm-text-muted);  border-color: var(--tm-border); }
      .tm-quest-inactive { opacity: 0.6; }
      .tm-quest-steps { margin: 8px 0 4px; padding-left: 20px; color: var(--tm-text); font-size: 13px; }
      .tm-quest-steps li { margin: 2px 0; }
      .tm-quest-step-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
      .tm-quest-step-row { display: flex; align-items: center; gap: 8px; }
      .tm-quest-step-num { width: 22px; height: 22px; flex: 0 0 22px; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; background: var(--tm-accent-soft); color: var(--tm-accent-text); font-size: 12px; font-weight: 600; }
      .tm-quest-step-name { flex: 1; font-size: 13px; }
      .tm-avatar-rows { display: flex; flex-direction: column; gap: 10px; }
      .tm-avatar-row { display: flex; align-items: center; gap: 8px; }
      .tm-avatar-row-icon { flex: 0 0 32px; font-size: 24px; color: var(--tm-accent-text); }
      .tm-avatar-row-fields { flex: 1; display: grid; grid-template-columns: 1.2fr 1.2fr 1fr 0.7fr; gap: 6px; }
      @media (max-width: 600px) { .tm-avatar-row-fields { grid-template-columns: 1fr 1fr; } }
      .tm-pill-alternating, .tm-pill-random, .tm-pill-balanced { background: var(--tm-accent-soft); color: var(--tm-accent-text); border-color: var(--tm-accent-border); }
      .tm-pill-first_come { background: var(--tm-gold-soft); color: var(--tm-gold); border-color: color-mix(in srgb, var(--tm-gold), transparent 75%); }
      .tm-pill-unassigned { background: var(--tm-muted-bg, #f0f0f0); color: var(--tm-text-muted); border-color: var(--tm-border); }
      .tm-pill-dot::before {
        content: ""; width: 5px; height: 5px; border-radius: 50%;
        background: currentColor;
      }

      /* ID badge (Show IDs toggle) */
      .tm-id-badge {
        display: inline-flex; align-items: center; gap: 4px;
        margin-top: 2px;
      }
      .tm-id-badge code {
        font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
        font-size: 10.5px; color: var(--tm-text-faint);
        background: var(--tm-surface-2); padding: 1px 6px;
        border-radius: 4px; user-select: all;
      }
      .tm-id-copy {
        background: none; border: none; cursor: pointer;
        padding: 2px; border-radius: 4px;
        color: var(--tm-text-faint); display: inline-flex; align-items: center;
        transition: color 0.15s;
      }
      .tm-id-copy:hover { color: var(--tm-accent); }
      .tm-id-copy ha-icon { --mdc-icon-size: 14px; }

      /* Reward extras */
      .tm-reward-card { display: flex; flex-direction: column; gap: 12px; margin-bottom: 0; }
      .tm-progress {
        height: 8px;
        background: var(--tm-surface-2);
        border-radius: 999px;
        overflow: hidden;
        margin-top: 4px;
        border: 1px solid var(--tm-border-soft);
      }
      .tm-progress > span {
        display: block; height: 100%;
        background: var(--tm-accent);
        border-radius: 999px;
      }
      .tm-progress-text {
        font-size: 12px; color: var(--tm-text-faint);
        display: flex; justify-content: space-between;
        font-variant-numeric: tabular-nums;
        margin-top: 6px;
      }
      .tm-progress-text strong { color: var(--tm-text); font-weight: 600; }

      /* Empty state */
      .tm-empty {
        text-align: center;
        padding: 56px 24px;
        background: var(--tm-surface-0);
        border: 1px dashed var(--tm-border-strong);
        border-radius: var(--tm-radius-lg);
      }
      .tm-empty-icon {
        width: 48px; height: 48px;
        border-radius: 12px;
        background: var(--tm-surface-2);
        display: inline-grid; place-items: center;
        font-size: 22px;
        margin-bottom: 12px;
        border: 1px solid var(--tm-border);
      }
      .tm-empty h3 { margin: 0 0 4px; font-size: 14px; font-weight: 600; letter-spacing: -0.005em; color: var(--tm-text); }
      .tm-empty p  { margin: 0 0 16px; color: var(--tm-text-muted); font-size: 13px; max-width: 360px; margin-left: auto; margin-right: auto; }

      /* Group list */
      .tm-group-list { margin: 8px 0 0; padding-left: 20px; color: var(--tm-text-muted); font-size: 13px; }
      .tm-group-list li { margin-bottom: 2px; }

      /* Activity tab */
      .tm-approval-list { display: flex; flex-direction: column; gap: 8px; }
      .tm-approval-item {
        display: flex; align-items: center; gap: 14px;
        padding: 12px;
        background: var(--tm-surface-2);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
      }
      .tm-approval-icon {
        width: 36px; height: 36px;
        border-radius: 50%;
        background: var(--tm-warning-soft);
        color: var(--tm-warning);
        display: grid; place-items: center;
        flex-shrink: 0;
      }
      .tm-approval-icon ha-icon { --mdc-icon-size: 20px; }
      .tm-approval-body { flex: 1; min-width: 0; }
      .tm-approval-photo img { width: 44px; height: 44px; object-fit: cover; border-radius: 8px; display: block; }
      .tm-approval-line { font-size: 13px; }
      .tm-approval-actions { display: flex; gap: 6px; flex-shrink: 0; }

      .tm-timeline { display: flex; flex-direction: column; }
      .tm-timeline-row {
        display: grid; grid-template-columns: 80px 32px 1fr auto;
        gap: 12px; align-items: center;
        padding: 10px 0;
        border-top: 1px solid var(--tm-border-soft);
        font-size: 13px;
      }
      .tm-timeline-row:first-child { border-top: 0; }
      .tm-timeline-time { color: var(--tm-text-faint); font-size: 12px; font-feature-settings: "tnum"; }
      .tm-timeline-icon {
        width: 28px; height: 28px;
        border-radius: 8px;
        display: grid; place-items: center;
        background: var(--tm-surface-2);
      }
      .tm-timeline-icon ha-icon { --mdc-icon-size: 16px; }
      .tm-timeline-completion ha-icon { color: var(--tm-positive); }
      .tm-timeline-claim ha-icon      { color: var(--tm-gold); }
      .tm-timeline-manual ha-icon     { color: var(--tm-accent); }
      .tm-timeline-points { font-weight: 600; }

      /* Settings */
      .tm-settings { display: flex; flex-direction: column; gap: 16px; }
      .tm-section {
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-lg);
        box-shadow: var(--tm-shadow-xs);
      }
      .tm-section-head {
        padding: 16px 20px 12px;
      }
      .tm-section-head h3 { margin: 0 0 3px; font-size: 14px; font-weight: 600; letter-spacing: -0.005em; }
      .tm-section-head p  { margin: 0; color: var(--tm-text-faint); font-size: 12.5px; }
      .tm-section-body { padding: 0; }
      .tm-setting-row {
        display: grid; grid-template-columns: 280px 1fr;
        gap: 20px; align-items: center;
        padding: 14px 20px;
        border-top: 1px solid var(--tm-border-soft);
      }
      .tm-setting-row:first-child { border-top: 0; }
      .tm-role-row { grid-template-columns: 1fr auto; cursor: pointer; }
      .tm-role-check {
        width: 20px; height: 20px; margin: 0;
        justify-self: end; accent-color: var(--tm-accent); cursor: pointer;
      }
      .tm-setting-label { color: var(--tm-text); font-size: 13px; font-weight: 500; }
      .tm-setting-label small { display: block; color: var(--tm-text-faint); font-weight: 400; font-size: 12px; margin-top: 2px; }
      .tm-difficulty-mults { display: flex; gap: 12px; flex-wrap: wrap; }
      .tm-difficulty-mults label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--tm-text-faint); }
      .tm-difficulty-mults .tm-input { width: 90px; }
      .tm-setting-row ha-textfield,
      .tm-setting-row ha-select,
      .tm-setting-row input.tm-input,
      .tm-setting-row select.tm-select {
        max-width: 360px;
      }
      .tm-time-pair {
        display: flex; align-items: center; gap: 8px;
      }
      .tm-time-pair .tm-input { max-width: 130px; }
      .tm-time-pair span { color: var(--tm-text-faint); }
      .tm-period-row {
        grid-template-columns: 56px minmax(140px, 1fr) auto 36px;
        gap: 12px;
      }
      .tm-period-row ha-icon-picker {
        width: 56px; max-width: 56px; height: 56px;
        border-radius: var(--tm-radius-sm, 8px); overflow: hidden;
        /* Older HA renders the picker as a filled mwc-textfield: drop the fill
           and its dark active-indicator line so no stray underline shows. */
        --mdc-text-field-fill-color: transparent;
        --mdc-text-field-idle-line-color: transparent;
        --mdc-text-field-hover-line-color: transparent;
        --mdc-text-field-focused-line-color: transparent;
        --mdc-text-field-disabled-line-color: transparent;
      }
      .tm-period-row > .tm-input { max-width: none; }
      .tm-vacation-row {
        grid-template-columns: minmax(140px, 1fr) auto 36px;
        gap: 12px;
      }
      .tm-vacation-row > .tm-input { max-width: none; }
      .tm-bulk-bar {
        display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
        padding: 10px 16px; margin: 0 0 10px;
        background: var(--secondary-background-color, #f1f1f1);
        border-radius: 10px;
      }
      .tm-bulk-count { font-weight: 600; margin-right: auto; }
      .tm-btn-on { background: var(--primary-color, #5b8def); color: #fff; }
      .tm-audit-row {
        display: grid;
        grid-template-columns: 180px 130px 180px 1fr;
        gap: 12px;
        padding: 8px 20px;
        border-top: 1px solid var(--tm-border-soft);
        font-size: 13px;
      }
      .tm-audit-row:first-child { border-top: 0; }
      .tm-audit-head { font-weight: 600; color: var(--tm-text-faint); }
      .tm-audit-when, .tm-audit-user { color: var(--tm-text-faint); }
      .tm-audit-action { font-family: var(--code-font-family, monospace); }
      @media (max-width: 700px) {
        .tm-audit-row { grid-template-columns: 1fr 1fr; }
        .tm-audit-head { display: none; }
      }
      .tm-period-del {
        color: var(--tm-text-faint);
      }
      .tm-period-del:not([disabled]):hover { color: var(--error-color, #d9434f); }
      .tm-period-del[disabled] { opacity: .5; cursor: not-allowed; }
      .tm-period-inuse {
        grid-column: 1 / -1;
        font-size: 12px; color: var(--tm-text-faint);
        margin-top: -6px;
      }
      .tm-period-anytime {
        grid-template-columns: 56px 1fr;
        color: var(--tm-text-faint);
      }
      .tm-period-anytime ha-icon { margin: 0 auto; }
      .tm-period-actions {
        display: flex; gap: 10px;
        padding: 14px 20px;
        border-top: 1px solid var(--tm-border-soft);
      }
      @media (max-width: 700px) {
        .tm-period-row { grid-template-columns: 48px 1fr 36px; }
        .tm-period-row .tm-time-pair { grid-column: 1 / -1; }
      }
      .tm-settings-foot {
        display: flex; justify-content: flex-end;
        padding-top: 8px;
      }

      /* Switch (HA native ha-switch) */
      ha-switch {
        --mdc-theme-secondary: var(--primary-color);
      }

      /* Dialog */
      .tm-scrim {
        position: fixed; inset: 0;
        background: rgba(0,0,0,0.5);
        display: flex; align-items: flex-start; justify-content: center;
        padding: 60px 20px; z-index: 100;
        overflow-y: auto;
        animation: tm-scrim-in 0.18s var(--tm-easing);
      }
      @keyframes tm-scrim-in { from { opacity: 0; } to { opacity: 1; } }
      .tm-dialog {
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-lg);
        width: 100%; max-width: 560px;
        box-shadow: var(--tm-shadow-lg);
        display: flex; flex-direction: column;
        max-height: calc(100vh - 120px);
        overflow: hidden;
        animation: tm-dialog-in 0.2s var(--tm-easing);
      }
      @keyframes tm-dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.985); } to { opacity: 1; transform: none; } }

      .tm-dialog-head {
        padding: 18px 22px;
        border-bottom: 1px solid var(--tm-border-soft);
        display: flex; align-items: center;
      }
      .tm-dialog-head h2 {
        margin: 0; font-size: 16px; font-weight: 600;
        letter-spacing: -0.01em;
        flex: 1;
      }
      .tm-dialog-body { padding: 16px 20px; overflow-y: auto; }
      .tm-dialog-foot {
        padding: 12px 20px;
        border-top: 1px solid var(--tm-border-soft);
        background: var(--tm-surface-2);
        display: flex; justify-content: flex-end; gap: 8px;
      }

      .tm-field { display: block; margin-bottom: 14px; }
      .tm-field-label {
        display: block; color: var(--tm-text);
        font-size: 12.5px; margin-bottom: 5px;
        font-weight: 500;
      }
      .tm-field ha-textfield, .tm-field ha-select {
        display: block; width: 100%;
      }
      .tm-input, .tm-select {
        width: 100%; box-sizing: border-box;
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
        padding: 10px 12px;
        color: var(--tm-text);
        font-size: 13px; font-family: inherit;
        box-shadow: var(--tm-shadow-xs);
        transition: all 0.1s var(--tm-easing);
      }
      .tm-input:hover, .tm-select:hover { border-color: var(--tm-border-strong); }
      .tm-input:focus, .tm-select:focus { outline: 0; border-color: var(--tm-accent); box-shadow: var(--tm-shadow-focus); }
      .tm-input::placeholder { color: var(--tm-text-vfaint); }
      .tm-select {
        cursor: pointer;
        appearance: none; -webkit-appearance: none;
        padding-right: 32px;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23888' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: right 10px center;
      }
      .tm-select option { background: var(--tm-surface-0); color: var(--tm-text); }
      .tm-textarea {
        width: 100%;
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
        padding: 7px 10px;
        color: var(--tm-text);
        font-size: 13px;
        font-family: inherit;
        box-sizing: border-box;
        box-shadow: var(--tm-shadow-xs);
        transition: all 0.1s var(--tm-easing);
      }
      .tm-textarea:focus {
        outline: 0;
        border-color: var(--tm-accent);
        box-shadow: var(--tm-shadow-focus);
      }
      .tm-ep { position: relative; }
      .tm-ep-search-wrap {
        display: flex; align-items: center;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm); padding: 0 10px;
        box-shadow: var(--tm-shadow-xs); transition: all 0.1s var(--tm-easing);
      }
      .tm-ep-search-wrap:focus-within {
        border-color: var(--tm-accent);
        box-shadow: var(--tm-shadow-focus);
      }
      .tm-ep-search-icon {
        --mdc-icon-size: 18px;
        color: var(--tm-text-faint); flex-shrink: 0; margin-right: 6px;
      }
      .tm-ep-input {
        flex: 1; border: 0; background: transparent; outline: 0;
        color: var(--tm-text); font-size: 13px; font-family: inherit;
        padding: 8px 0; width: 100%;
      }
      .tm-ep-input::placeholder { color: var(--tm-text-faint); }
      .tm-ep-selected {
        display: flex; align-items: center; gap: 8px;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm); padding: 6px 10px;
        cursor: pointer; transition: all 0.1s var(--tm-easing);
        box-shadow: var(--tm-shadow-xs);
      }
      .tm-ep-selected:hover { border-color: var(--tm-accent); }
      .tm-ep-selected-icon { --mdc-icon-size: 20px; color: var(--tm-text-muted); flex-shrink: 0; }
      .tm-ep-selected-text {
        flex: 1; font-size: 13px; color: var(--tm-text);
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      }
      .tm-entity-clear {
        width: 20px; height: 20px;
        border: 0; background: var(--tm-surface-2);
        border-radius: 50%; cursor: pointer;
        font-size: 11px; color: var(--tm-text-faint);
        display: grid; place-items: center;
        font-family: inherit; flex-shrink: 0;
      }
      .tm-entity-clear:hover { background: var(--tm-danger-soft); color: var(--tm-danger); }
      .tm-ep-dropdown {
        position: absolute; top: 100%; left: 0; right: 0;
        max-height: 260px; overflow-y: auto;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius); z-index: 200;
        margin-top: 4px; padding: 4px 0;
        box-shadow: var(--tm-shadow-lg);
      }
      .tm-ep-option {
        display: flex; align-items: center; gap: 10px;
        padding: 8px 12px; cursor: pointer;
        transition: background 0.08s;
      }
      .tm-ep-option:hover { background: var(--tm-surface-hover); }
      .tm-ep-option-icon { --mdc-icon-size: 20px; color: var(--tm-text-muted); flex-shrink: 0; }
      .tm-ep-option-text { flex: 1; min-width: 0; }
      .tm-ep-id { font-size: 13px; color: var(--tm-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .tm-ep-name { font-size: 11px; color: var(--tm-text-muted); margin-top: 1px; font-family: ui-monospace, monospace; }
      .tm-field-hint {
        display: block; color: var(--tm-text-faint);
        font-size: 11.5px; margin-top: 4px; line-height: 1.5;
      }
      .tm-field-hint code {
        font-family: ui-monospace, "SF Mono", Menlo, monospace;
        background: var(--tm-surface-2); padding: 1px 5px;
        border-radius: 3px; font-size: 11px;
      }
      .tm-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
      .tm-textarea { resize: vertical; min-height: 100px; line-height: 1.5; }

      .tm-field ha-icon-picker,
      .tm-section-body ha-icon-picker {
        display: block; width: 100%;
      }

      /* Chip multi-select */
      /* Insight reports (#679) */
      .tm-verdict {
        border-radius: 10px; padding: 10px 14px; margin: 10px 0 14px;
        font-weight: 700; font-size: 13.5px;
      }
      .tm-verdict-ok { background: rgba(46, 125, 50, 0.12); color: #2e7d32; }
      .tm-verdict-warn { background: rgba(239, 108, 0, 0.14); color: #ef6c00; }
      .tm-fairness { display: flex; flex-direction: column; gap: 14px; }
      .tm-fair-row { display: grid; grid-template-columns: 120px 1fr auto; gap: 10px 12px; align-items: center; }
      .tm-fair-name { font-weight: 700; font-size: 14px; }
      .tm-fair-bar {
        position: relative; height: 14px; border-radius: 99px;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        overflow: visible;
      }
      .tm-fair-bar > i {
        display: block; height: 100%; border-radius: 99px;
        background: var(--tm-accent); min-width: 2px;
      }
      .tm-fair-bar > i.tm-fair-over { background: #ef6c00; }
      .tm-fair-bar > i.tm-fair-under { background: #7e57c2; }
      .tm-fair-bar > i.tm-fair-balanced { background: #2e7d32; }
      /* Marks where an even split would sit, so "fair" is visible not implied. */
      .tm-fair-target {
        position: absolute; top: -3px; width: 2px; height: 20px;
        background: var(--tm-text-muted); opacity: 0.65;
      }
      .tm-fair-figs { grid-column: 2; font-size: 12.5px; color: var(--tm-text-muted); }
      .tm-fair-status {
        font-size: 11.5px; font-weight: 800; text-transform: uppercase;
        letter-spacing: 0.03em; white-space: nowrap;
      }
      .tm-fair-status.tm-fair-over { color: #ef6c00; }
      .tm-fair-status.tm-fair-under { color: #7e57c2; }
      .tm-fair-status.tm-fair-balanced { color: #2e7d32; }
      .tm-fair-status.tm-fair-idle { color: var(--tm-text-muted); }
      @media (max-width: 640px) {
        .tm-fair-row { grid-template-columns: 1fr auto; }
        .tm-fair-bar { grid-column: 1 / -1; }
      }

      /* Health & diagnostics (#682) */
      .tm-health-issues { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
      .tm-health-issue {
        display: flex; align-items: center; gap: 10px;
        padding: 10px 12px; border-radius: 10px;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
      }
      .tm-health-sev {
        font-size: 11px; font-weight: 800; text-transform: uppercase;
        padding: 2px 8px; border-radius: 999px; white-space: nowrap;
      }
      .tm-health-error .tm-health-sev { background: rgba(198,40,40,.14); color: #c62828; }
      .tm-health-warning .tm-health-sev { background: rgba(239,108,0,.16); color: #ef6c00; }
      .tm-health-info .tm-health-sev { background: var(--tm-surface-1, #eee); color: var(--tm-text-muted); }
      .tm-health-msg { flex: 1; font-size: 13.5px; line-height: 1.45; }
      .tm-btn-small { padding: 4px 10px; font-size: 12px; }
      .tm-health-counts {
        display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
        gap: 10px;
      }
      .tm-health-stat {
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: 10px; padding: 10px; text-align: center;
      }
      .tm-health-num { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; }
      .tm-health-label { font-size: 11.5px; color: var(--tm-text-muted); margin-top: 2px; }

      /* Projection report (#681) */
      .tm-proj-totals { display: flex; flex-direction: column; gap: 12px; margin: 12px 0 16px; }
      .tm-proj-kid { display: grid; grid-template-columns: 120px 1fr; gap: 6px 12px; align-items: center; }
      .tm-proj-kid .tm-fair-figs { grid-column: 2; }
      .tm-projection td, .tm-projection th { font-size: 13px; text-align: center; }
      .tm-projection td:first-child, .tm-projection th:first-child { text-align: left; }
      .tm-proj-date { color: var(--tm-text-muted); font-size: 11.5px; }
      .tm-proj-count { color: var(--tm-text-muted); font-size: 11.5px; }

      /* Friction report (#680) */
      .tm-friction td, .tm-friction th { font-size: 13px; }
      .tm-fric-verdict {
        display: inline-block; padding: 2px 8px; border-radius: 999px;
        font-size: 11.5px; font-weight: 800; text-transform: uppercase;
      }
      .tm-fric-never    { background: rgba(198, 40, 40, 0.14); color: #c62828; }
      .tm-fric-stalling { background: rgba(239, 108, 0, 0.16); color: #ef6c00; }
      .tm-fric-struggling { background: rgba(251, 192, 45, 0.20); color: #a67c00; }
      .tm-fric-fine     { background: rgba(46, 125, 50, 0.14); color: #2e7d32; }
      .tm-fric-unknown  { background: var(--tm-surface-0); color: var(--tm-text-muted); }
      .tm-fric-misses { color: #ef6c00; font-weight: 800; font-size: 12px; }

      /* Timed unlock allowlist (#678) */
      .tm-setting-stack { flex-direction: column; align-items: stretch; }
      .tm-allowlist { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
      .tm-allow-chip {
        display: inline-flex; align-items: center; gap: 6px;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: 999px; padding: 4px 6px 4px 12px;
        font-size: 12.5px; font-family: var(--tm-mono, monospace);
      }
      .tm-allow-chip button {
        background: none; border: 0; cursor: pointer; padding: 0 2px;
        color: var(--tm-text-muted); font-size: 13px; line-height: 1;
      }

      /* Scheduled config changes (#675) */
      .tm-sched-count {
        display: inline-block; min-width: 18px; padding: 0 5px;
        background: var(--tm-accent); color: #fff;
        border-radius: 999px; font-size: 11px; font-weight: 700;
        text-align: center; vertical-align: middle;
      }
      .tm-sched-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
      .tm-sched-row {
        display: flex; align-items: flex-start; gap: 10px;
        padding: 8px 10px; border-radius: 8px;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
      }
      .tm-sched-when {
        font-variant-numeric: tabular-nums; font-weight: 700;
        font-size: 12.5px; white-space: nowrap; padding-top: 1px;
      }
      .tm-sched-what { flex: 1; font-size: 12.5px; line-height: 1.5; }
      .tm-sched-note { color: var(--tm-text-muted); font-size: 11.5px; margin-top: 2px; }
      .tm-sched-add {
        border-top: 1px solid var(--tm-border);
        padding-top: 10px; margin-top: 4px;
      }
      .tm-sched-applied { margin-top: 14px; }
      .tm-sched-done { opacity: 0.65; }
      .tm-sched-tick { color: var(--tm-success, #2e7d32); font-weight: 800; }

      .tm-chip-row { display: flex; gap: 6px; flex-wrap: wrap; }
      .tm-chip-btn {
        background: var(--tm-surface-0);
        color: var(--tm-text-muted);
        border: 1px solid var(--tm-border);
        padding: 4px 10px; border-radius: 999px;
        font-size: 12.5px; font-weight: 500;
        font-family: inherit;
        cursor: pointer;
        transition: all 0.1s var(--tm-easing);
      }
      .tm-chip-btn:hover { color: var(--tm-text); border-color: var(--tm-border-strong); }
      .tm-chip-on {
        background: var(--tm-accent-soft);
        color: var(--tm-accent-text);
        border-color: var(--tm-accent-border);
      }

      .tm-day-row { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
      .tm-day-btn {
        background: var(--tm-surface-0);
        color: var(--tm-text-muted);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
        padding: 8px 0;
        font-size: 11.5px; font-weight: 600;
        font-family: inherit; cursor: pointer;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        transition: all 0.1s var(--tm-easing);
      }
      .tm-day-btn:hover { color: var(--tm-text); border-color: var(--tm-border-strong); }
      .tm-day-on {
        background: var(--tm-accent); color: var(--text-primary-color, #fff);
        border-color: var(--tm-accent);
      }

      .tm-check-row {
        display: flex; align-items: flex-start; gap: 14px;
        padding: 12px 0;
        border-top: 1px solid var(--tm-border-soft);
      }
      .tm-check-row:first-child { border-top: 0; padding-top: 0; }
      .tm-check-row > div { flex: 1; }
      .tm-check-title { font-size: 13px; color: var(--tm-text); font-weight: 500; }

      details.tm-advanced {
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
        margin-top: 16px;
        overflow: hidden;
      }
      details.tm-advanced summary {
        cursor: pointer; padding: 12px 14px;
        color: var(--tm-text-muted); font-size: 12px; font-weight: 500;
        user-select: none; list-style: none;
        display: flex; align-items: center; gap: 8px;
      }
      details.tm-advanced summary::-webkit-details-marker { display: none; }
      details.tm-advanced summary::before {
        content: "▸";
        transition: transform 0.15s var(--tm-easing);
        color: var(--tm-text-faint);
        font-size: 10px;
      }
      details.tm-advanced[open] summary::before { transform: rotate(90deg); }
      details.tm-advanced > div {
        padding: 14px;
        border-top: 1px solid var(--tm-border-soft);
      }

      /* Reorder dialog */
      .tm-reorder-list { display: flex; flex-direction: column; gap: 4px; margin-top: 12px; }
      .tm-reorder-item {
        display: flex; align-items: center; gap: 12px;
        padding: 10px 12px;
        background: var(--tm-surface-0);
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
        cursor: grab;
        transition: all 0.12s var(--tm-easing);
      }
      .tm-reorder-item:hover { border-color: var(--tm-accent); background: var(--tm-surface-2); }
      .tm-reorder-item.tm-dragging { opacity: 0.4; }
      .tm-reorder-handle { color: var(--tm-text-faint); cursor: grab; font-size: 18px; user-select: none; }
      .tm-reorder-name { flex: 1; font-size: 13px; font-weight: 500; }
      .tm-reorder-points { color: var(--tm-accent); font-weight: 600; font-size: 13px; }
      .tm-reorder-moves { display: flex; flex-direction: column; gap: 2px; }
      .tm-reorder-move {
        display: flex; align-items: center; justify-content: center;
        width: 28px; height: 20px; padding: 0;
        border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm);
        background: var(--tm-surface-1);
        color: var(--tm-text);
        font-size: 10px; line-height: 1;
        cursor: pointer;
        transition: all 0.12s var(--tm-easing);
      }
      .tm-reorder-move:hover:not(:disabled) { border-color: var(--tm-accent); color: var(--tm-accent); }
      .tm-reorder-move:disabled { opacity: 0.3; cursor: default; }

      /* Toast */
      .tm-toast {
        position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
        padding: 10px 16px; border-radius: var(--tm-radius);
        font-size: 13px; font-weight: 500;
        box-shadow: var(--tm-shadow);
        z-index: 200;
        animation: tm-toast-in 0.2s var(--tm-easing);
      }
      @keyframes tm-toast-in { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translateX(-50%); } }
      .tm-toast-ok  { background: var(--tm-positive); color: var(--text-primary-color, #fff); }
      .tm-toast-err { background: var(--tm-danger); color: var(--text-primary-color, #fff); }

      /* Templates */
      .tm-section-divider {
        display: flex; align-items: center; gap: 12px;
        margin: 20px 0 14px; font-size: 11.5px; font-weight: 600;
        color: var(--tm-text-faint); text-transform: uppercase; letter-spacing: 0.04em;
      }
      .tm-section-divider::after { content: ''; flex: 1; height: 1px; background: var(--tm-border); }
      .tm-tpl-icon {
        width: 40px; height: 40px; border-radius: 10px;
        background: var(--tm-accent-soft); border: 1px solid var(--tm-border);
        display: grid; place-items: center; flex-shrink: 0;
        color: var(--tm-accent); --mdc-icon-size: 20px;
      }
      .tm-tpl-picker-card { cursor: pointer; display: flex; flex-direction: column; gap: 10px; }
      .tm-tpl-picker-card:hover { border-color: var(--tm-accent); background: var(--tm-accent-soft); }
      .tm-tpl-picker-head { display: flex; align-items: center; gap: 12px; }
      .tm-tpl-picker-name { font-size: 15px; font-weight: 600; }
      .tm-tpl-chore-pills { display: flex; flex-wrap: wrap; gap: 5px; }
      .tm-tpl-chore-pill {
        font-size: 11px; padding: 2px 8px;
        background: var(--tm-surface-2); border-radius: 999px;
        color: var(--tm-text-muted); border: 1px solid var(--tm-border-soft);
      }
      .tm-manage-tpl {
        display: flex; align-items: center; gap: 14px;
        padding: 14px 18px; background: var(--tm-surface-0);
        border: 1px solid var(--tm-border); border-radius: var(--tm-radius-lg);
        margin-bottom: 10px; transition: all 0.15s;
      }
      .tm-manage-tpl:hover { border-color: var(--tm-border-strong); box-shadow: var(--tm-shadow-sm); }
      .tm-manage-tpl-locked { opacity: 0.65; }
      .tm-manage-tpl-locked:hover { border-color: var(--tm-border); box-shadow: none; }
      .tm-manage-tpl-info { flex: 1; }
      .tm-manage-tpl-name { font-size: 14px; font-weight: 600; }
      .tm-tpl-preview-card {
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-lg); overflow: hidden; margin-bottom: 10px;
      }
      .tm-tpl-preview-card:hover { border-color: var(--tm-border-strong); }
      .tm-tpl-preview-header {
        display: flex; align-items: center; gap: 12px;
        padding: 12px 16px; cursor: pointer;
      }
      .tm-tpl-preview-header:hover { background: var(--tm-surface-hover); }
      .tm-tpl-expand { font-size: 11px; color: var(--tm-text-faint); transition: transform 0.2s; }
      .tm-tpl-expand.open { transform: rotate(90deg); }
      .tm-tpl-preview-name { font-size: 14px; font-weight: 600; flex: 1; }
      .tm-tpl-preview-summary { font-size: 12px; color: var(--tm-text-muted); display: flex; gap: 12px; }
      .tm-tpl-remove {
        width: 26px; height: 26px; border: none; background: transparent;
        border-radius: var(--tm-radius-sm); color: var(--tm-text-faint);
        cursor: pointer; display: grid; place-items: center; font-size: 14px;
      }
      .tm-tpl-remove:hover { background: var(--tm-danger-soft); color: var(--tm-danger); }
      .tm-tpl-preview-body { padding: 14px 16px; border-top: 1px solid var(--tm-border-soft); }
      .tm-tpl-confirm-bar {
        display: flex; align-items: center; justify-content: space-between; gap: 12px;
        padding: 16px 18px; margin-top: 16px;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-lg);
      }
      .tm-tpl-confirm-bar div { font-size: 13px; color: var(--tm-text-muted); }
      .tm-tpl-confirm-bar strong { color: var(--tm-text); }
      .tm-tpl-checklist { max-height: 320px; overflow-y: auto; }
      .tm-tpl-check-row {
        display: flex; align-items: center; gap: 10px;
        padding: 8px 0; border-bottom: 1px solid var(--tm-border-soft);
        font-size: 13px; cursor: pointer;
      }
      .tm-tpl-check-row:last-child { border-bottom: none; }
      .tm-tpl-check-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--tm-accent); }
      .tm-day-pills { display: flex; gap: 6px; flex-wrap: wrap; }
      .tm-day-pill {
        padding: 5px 12px; border: 1px solid var(--tm-border); border-radius: 999px;
        font-size: 12px; font-weight: 500; cursor: pointer;
        background: var(--tm-surface-0); color: var(--tm-text-muted); transition: all 0.1s;
        user-select: none;
      }
      .tm-day-pill.active { background: var(--tm-accent); color: var(--text-primary-color, #fff); border-color: var(--tm-accent); }

      /* ===== Badges ===== */
      .tm-badge-subtabs {
        display: flex; gap: 2px; border-bottom: 1px solid var(--tm-border);
        margin-bottom: 16px;
      }
      .tm-badge-subtab {
        padding: 9px 14px; font-size: 13px; font-weight: 500;
        color: var(--tm-text-muted); background: transparent; border: 0;
        border-bottom: 2px solid transparent; margin-bottom: -1px;
        cursor: pointer; font-family: inherit; transition: color 0.1s;
      }
      .tm-badge-subtab:hover { color: var(--tm-text); }
      .tm-badge-subtab-active { color: var(--tm-accent); border-bottom-color: var(--tm-accent); }

      .tm-badge-row td { vertical-align: middle; }

      /* tier colour token */
      .tm-badge-tier-bronze   { --badge-t: #cd7f32; }
      .tm-badge-tier-silver   { --badge-t: #c0c0c0; }
      .tm-badge-tier-gold     { --badge-t: #f1c40f; }
      .tm-badge-tier-platinum { --badge-t: #67e8f9; }

      .tm-badge-icon-sm {
        width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;
        background: var(--badge-t, #cd7f32);
        display: inline-flex; align-items: center; justify-content: center;
        color: #1a1a1a; --mdc-icon-size: 18px; vertical-align: middle;
      }
      .tm-badge-tier-label {
        font-size: 10.5px; font-weight: 700; text-transform: uppercase;
        letter-spacing: 1px; color: var(--badge-t, #cd7f32);
      }
      .tm-badge-criteria {
        font-family: ui-monospace, monospace; font-size: 11.5px;
        color: var(--tm-text-muted); white-space: nowrap;
      }
      .tm-badge-toggle {
        width: 36px; height: 20px; border-radius: 999px;
        background: var(--tm-border-strong); border: 0; cursor: pointer;
        position: relative; transition: background 0.15s; flex-shrink: 0;
      }
      .tm-badge-toggle::after {
        content: ""; position: absolute; top: 2px; left: 2px;
        width: 16px; height: 16px; border-radius: 50%;
        background: #fff; transition: transform 0.15s;
      }
      .tm-badge-toggle-on { background: var(--tm-accent); }
      .tm-badge-toggle-on::after { transform: translateX(16px); }

      .tm-badge-src {
        font-size: 10px; font-weight: 700; padding: 2px 8px;
        border-radius: 999px; background: var(--tm-surface-2); color: var(--tm-text-muted);
      }
      .tm-badge-src-manual { background: #3a2a1a; color: #f1c40f; }
      .tm-badge-src-silent  { background: var(--tm-surface-2); color: var(--tm-text-faint); }
      .tm-badge-src-auto    { background: var(--tm-accent-soft); color: var(--tm-accent-text); }

      .tm-badge-criteria-block {
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm); padding: 12px; margin-top: 6px;
      }
      .tm-badge-criterion-row {
        display: grid; grid-template-columns: 1.4fr 30px 0.8fr 28px;
        gap: 6px; align-items: center; margin-bottom: 8px;
      }
      .tm-badge-op { font-size: 16px; text-align: center; color: var(--tm-text-muted); }

      /* ===== Notifications: custom card buttons ===== */
      .tm-notif-custom-card {
        margin-top: 10px;
        padding: 14px;
        background: var(--tm-surface-1);
        border: 1px solid var(--tm-border);
        border-radius: 10px;
      }
      .tm-notif-custom-row {
        display: grid;
        grid-template-columns: 1fr 110px 42px 32px;
        gap: 10px;
        align-items: center;
        margin-bottom: 10px;
      }
      .tm-notif-custom-msg { width: 100%; margin-bottom: 12px; }
      .tm-notif-toggle-group {
        display: flex;
        gap: 6px;
        flex-wrap: wrap;
        margin-bottom: 8px;
      }
      .tm-notif-toggle-group:last-child { margin-bottom: 0; }
      .tm-notif-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        padding: 7px 14px;
        font-size: 12.5px;
        font-weight: 500;
        font-family: inherit;
        line-height: 1.2;
        background: var(--tm-surface-0);
        color: var(--tm-text);
        border: 1px solid var(--tm-border);
        transition: background 0.12s var(--tm-easing), color 0.12s var(--tm-easing), border-color 0.12s var(--tm-easing);
        user-select: none;
      }
      .tm-notif-toggle:hover {
        background: var(--tm-accent-soft);
        border-color: var(--tm-accent);
        color: var(--tm-accent-text);
      }
      .tm-notif-toggle input[type="checkbox"] { display: none; }
      .tm-notif-toggle-day { border-radius: var(--tm-radius-sm); min-width: 46px; }
      .tm-notif-toggle-pill { border-radius: 999px; }
      .tm-notif-toggle-on {
        background: var(--tm-accent);
        color: var(--text-primary-color, #fff);
        border-color: var(--tm-accent);
      }
      .tm-notif-toggle-on:hover {
        background: var(--tm-accent-hover);
        color: var(--text-primary-color, #fff);
        border-color: var(--tm-accent-hover);
      }

      /* Notification toggle switches (CSS-only on native checkbox) */
      .tm-notif-switch {
        -webkit-appearance: none;
        appearance: none;
        width: 38px; height: 22px;
        background: var(--tm-border-strong);
        border: none; border-radius: 999px;
        cursor: pointer; position: relative;
        transition: background 0.15s;
        flex-shrink: 0; vertical-align: middle;
        margin: 0;
      }
      .tm-notif-switch::after {
        content: ""; position: absolute;
        top: 3px; left: 3px;
        width: 16px; height: 16px; border-radius: 50%;
        background: #fff; transition: transform 0.15s;
      }
      .tm-notif-switch:checked { background: var(--tm-accent); }
      .tm-notif-switch:checked::after { transform: translateX(16px); }
      .tm-notif-switch:disabled { opacity: 0.35; cursor: not-allowed; }

      /* Notification matrix cells */
      .tm-notif-matrix-cell { text-align: center; vertical-align: middle; }
      .tm-notif-time-input {
        display: block; margin: 6px auto 0; width: 86px;
        font-size: 12px; padding: 5px 6px; text-align: center;
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm); color: var(--tm-text);
        font-family: inherit; transition: border-color 0.1s;
      }
      .tm-notif-time-input:focus {
        outline: 0; border-color: var(--tm-accent);
        box-shadow: var(--tm-shadow-focus);
      }
      .tm-notif-time-input:disabled { opacity: 0.35; }

      /* Notification recipient selects */
      .tm-notif-select {
        background: var(--tm-surface-0); border: 1px solid var(--tm-border);
        border-radius: var(--tm-radius-sm); padding: 7px 10px;
        color: var(--tm-text); font-size: 12.5px; font-family: inherit;
        cursor: pointer; min-width: 0; max-width: 240px; transition: border-color 0.1s;
      }
      .tm-notif-select:hover { border-color: var(--tm-border-strong); }
      .tm-notif-select:focus { outline: 0; border-color: var(--tm-accent); box-shadow: var(--tm-shadow-focus); }

      /* Notification delete button */
      .tm-notif-delete {
        width: 30px; height: 30px;
        border: 1px solid transparent; background: transparent;
        border-radius: var(--tm-radius-sm);
        display: grid; place-items: center;
        color: var(--tm-text-faint); cursor: pointer;
        font-size: 16px; font-family: inherit;
        transition: all 0.1s;
      }
      .tm-notif-delete:hover {
        background: var(--tm-danger-soft); color: var(--tm-danger);
        border-color: var(--tm-danger-soft);
      }

      /* Power user info box */
      .tm-notif-info-box {
        margin-top: 16px; padding: 14px 16px;
        background: var(--tm-surface-1); border: 1px solid var(--tm-border);
        border-left: 3px solid var(--tm-accent, #4fc3f7);
        border-radius: var(--tm-radius-sm);
        font-size: 12.5px; color: var(--tm-text-muted); line-height: 1.55;
      }
      .tm-notif-info-box strong {
        color: var(--tm-text); display: block;
        margin-bottom: 4px; font-size: 13px;
      }

      /* ===== Mobile / narrow ===== */
      @media (max-width: 900px) {
        .tm-shell { grid-template-columns: 1fr; }
        .tm-sidebar { display: none; }
        .tm-menu-btn { display: inline-flex; }
        .tm-topbar { padding: 0 12px; }
        /* Redundant with the section picker below — hide on mobile. */
        .tm-crumbs { display: none; }
        .tm-approval-pill { margin-left: auto; }

        /* Section picker replaces the old horizontal tab strip. */
        .tm-main { position: relative; }
        .tm-mobilenav {
          display: block;
          position: relative;
          z-index: 5;
          flex-shrink: 0;
          background: var(--tm-surface-0);
          border-bottom: 1px solid var(--tm-border);
          padding: 8px 12px;
        }
        .tm-mnav-trigger {
          width: 100%;
          display: flex; align-items: center; gap: 10px;
          background: var(--tm-surface-2);
          border: 1px solid var(--tm-border);
          border-radius: var(--tm-radius-md, 12px);
          padding: 8px 12px;
          cursor: pointer;
          font-family: inherit;
          color: var(--tm-text);
          text-align: left;
          transition: background 0.12s var(--tm-easing), border-color 0.12s var(--tm-easing);
        }
        .tm-mnav-trigger:active { background: var(--tm-surface-3); }
        .tm-mobilenav-open .tm-mnav-trigger { border-color: var(--tm-accent); }
        .tm-mnav-ico {
          width: 30px; height: 30px; flex-shrink: 0;
          border-radius: var(--tm-radius-sm, 8px);
          background: var(--tm-accent-soft);
          color: var(--tm-accent-text);
          display: grid; place-items: center;
          --mdc-icon-size: 20px;
        }
        .tm-mnav-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
        .tm-mnav-crumb { font-size: 11px; color: var(--tm-text-faint); line-height: 1.2; }
        .tm-mnav-title {
          font-size: 15px; font-weight: 600; line-height: 1.25;
          white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        }
        .tm-mnav-chev {
          color: var(--tm-text-muted); flex-shrink: 0;
          --mdc-icon-size: 22px;
          transition: transform 0.18s var(--tm-easing);
        }
        .tm-mobilenav-open .tm-mnav-chev { transform: rotate(180deg); }
        .tm-mnav-count {
          flex-shrink: 0;
          background: var(--tm-surface-0);
          border: 1px solid var(--tm-border);
          color: var(--tm-text-muted);
          font-size: 11px; font-weight: 600;
          padding: 1px 7px; border-radius: 999px;
        }
        .tm-mnav-count-urgent {
          background: var(--tm-warning-soft);
          border-color: transparent;
          color: var(--tm-warning);
        }
        .tm-mnav-scrim {
          position: absolute;
          top: 100%; left: 0; right: 0;
          height: 100vh;
          background: rgba(10, 15, 22, 0.32);
          z-index: 30;
          animation: tm-mnav-fade 0.18s var(--tm-easing);
        }
        .tm-mnav-sheet {
          position: absolute;
          top: calc(100% + 6px); left: 8px; right: 8px;
          z-index: 31;
          background: var(--tm-surface-0);
          border: 1px solid var(--tm-border);
          border-radius: var(--tm-radius-md, 14px);
          box-shadow: 0 20px 48px rgba(0, 0, 0, 0.24);
          max-height: calc(100vh - 200px);
          overflow-y: auto;
          padding: 4px 0 8px;
          animation: tm-mnav-drop 0.18s var(--tm-easing);
        }
        .tm-mnav-grp-head {
          font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
          text-transform: uppercase; color: var(--tm-text-faint);
          padding: 12px 16px 4px;
        }
        .tm-mnav-item {
          display: flex; align-items: center; gap: 12px; width: 100%;
          background: none; border: 0; font-family: inherit; cursor: pointer;
          padding: 11px 16px; color: var(--tm-text); text-align: left;
        }
        .tm-mnav-item:active { background: var(--tm-surface-2); }
        .tm-mnav-item-ico {
          flex-shrink: 0; color: var(--tm-text-muted);
          --mdc-icon-size: 20px;
        }
        .tm-mnav-item-label { flex: 1; font-size: 15px; }
        .tm-mnav-item-active { background: var(--tm-accent-soft); color: var(--tm-accent-text); }
        .tm-mnav-item-active .tm-mnav-item-ico { color: var(--tm-accent-text); }
        @keyframes tm-mnav-fade { from { opacity: 0; } to { opacity: 1; } }
        @keyframes tm-mnav-drop {
          from { opacity: 0; transform: translateY(-8px) scale(0.985); }
          to { opacity: 1; transform: none; }
        }
        .tm-body { padding: 16px; }
        .tm-toolbar { flex-direction: column; align-items: stretch; }
        .tm-search-wrap { max-width: none; }
        .tm-dialog { max-width: none; border-radius: 0; max-height: 100vh; height: 100vh; }
        .tm-scrim { padding: 0; }
        .tm-dialog-body .tm-field-row { grid-template-columns: 1fr; }
        .tm-setting-row { grid-template-columns: 1fr; gap: 6px; padding: 12px 16px; }
        .tm-role-row { grid-template-columns: 1fr auto; gap: 12px; align-items: center; }
        .tm-section-head, .tm-setting-row { padding-left: 16px; padding-right: 16px; }
        .tm-timeline-row { grid-template-columns: 1fr auto; }
        .tm-timeline-time, .tm-timeline-icon { display: none; }
      }
    </style>`;
  }
}

// Guard the define: after a version bump the browser can briefly hold both the
// old (?v=prev) and new (?v=current) panel modules, and a second unguarded
// define() throws "name already used", aborting module evaluation.
if (!customElements.get("taskmate-panel")) {
  customElements.define("taskmate-panel", TaskMatePanel);
}
