/**
 * TaskMate shared design layer.
 *
 * Injects design-token CSS (custom properties) keyed by [data-tm-design],
 * and exposes helpers for cards to resolve the active design + build the
 * editor dropdown. CSS custom properties inherit across shadow-DOM
 * boundaries, so a card only needs to set data-tm-design on its host and
 * consume var(--tmd-*) inside its shadow styles.
 *
 * Designs:
 *   classic  — current look; no tokens (cards fall back to HA theme vars)
 *   playroom — warm, rounded, picture-book
 *   console  — dark gamified HUD
 *   cleanpro — calm productivity / SaaS
 *   accessible — high contrast, colour-blind safe, dyslexia-friendly type
 *
 * Mirrors window.__taskmate_localize: exposed globally, no ES module imports.
 */
(function () {
  const IDS = ["classic", "playroom", "console", "cleanpro", "accessible"];

  // Fonts must be loaded at the document level (an @import inside a shadow
  // stylesheet is honoured, but loading once globally avoids N duplicate fetches).
  const FONT_IMPORT =
    "@import url('https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700;800&family=Nunito:wght@400;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700;800&family=Manrope:wght@500;600;700;800&family=Atkinson+Hyperlegible:wght@400;700&display=swap');";

  // Design tokens. IMPORTANT: these are :host-scoped so they can be applied
  // INSIDE each card's shadow root. A document-level [data-tm-design] rule
  // cannot reach a card host nested in HA's shadow trees, but :host() matches
  // the host carrying the attribute wherever it lives. Custom properties then
  // inherit down into the card's shadow DOM. Tokens mirror
  // docs/design/redesigns/taskmate-redesigns.html (.dir-a / .dir-b / .dir-c).
  const TOKENS = `
:host([data-tm-design="playroom"]),[data-tm-design="playroom"]{
  --tmd-bg:#FFF7EC;--tmd-surface:#FFFFFF;--tmd-surface-2:#FFF0DB;--tmd-border:#F0DFC4;
  --tmd-text:#3A2E26;--tmd-dim:#9C8676;
  --tmd-accent:#7C5CE6;--tmd-accent2:#FFC23C;--tmd-good:#36C58E;--tmd-warn:#FFB020;--tmd-bad:#FF6B6B;--tmd-gold:#FFC23C;
  --tmd-radius:22px;--tmd-radius-sm:14px;--tmd-shadow:0 10px 24px rgba(123,92,230,.12);--tmd-hd-text:#fff;
  --tmd-font-display:"Baloo 2",cursive;--tmd-font-body:"Nunito",sans-serif;--tmd-font-mono:"Baloo 2",cursive;
  --tmd-c1:#FF6B6B;--tmd-c2:#4ECDC4;--tmd-c3:#FFB020;--tmd-c4:#6C8DFF;--tmd-c5:#C77DFF;--tmd-c6:#45D483;
}
:host([data-tm-design="console"]),[data-tm-design="console"]{
  --tmd-bg:#EEF2F8;--tmd-surface:#FFFFFF;--tmd-surface-2:#E9EEF5;--tmd-border:#D3DCE7;
  --tmd-text:#0F1B2D;--tmd-dim:#5C6B80;
  --tmd-accent:#0E9BC4;--tmd-accent2:#E0357F;--tmd-good:#1AA06A;--tmd-warn:#C98800;--tmd-bad:#E0476A;--tmd-gold:#C98800;
  --tmd-radius:12px;--tmd-radius-sm:8px;--tmd-shadow:0 6px 18px rgba(18,32,52,.12);--tmd-hd-text:#fff;
  --tmd-font-display:"Space Grotesk",sans-serif;--tmd-font-body:"Inter",sans-serif;--tmd-font-mono:"JetBrains Mono",monospace;
  --tmd-c1:#0E9BC4;--tmd-c2:#E0357F;--tmd-c3:#C98800;--tmd-c4:#6A4BE0;--tmd-c5:#1AA06A;--tmd-c6:#E0703D;
}
:host([data-tm-design="cleanpro"]),[data-tm-design="cleanpro"]{
  --tmd-bg:#F6F7F9;--tmd-surface:#FFFFFF;--tmd-surface-2:#F1F3F6;--tmd-border:#E5E8EC;
  --tmd-text:#1A2230;--tmd-dim:#6B7585;
  --tmd-accent:#4F6BED;--tmd-accent2:#0FB5A8;--tmd-good:#16A36B;--tmd-warn:#E0A100;--tmd-bad:#DC4C4C;--tmd-gold:#E0A100;
  --tmd-radius:14px;--tmd-radius-sm:9px;--tmd-shadow:0 1px 2px rgba(16,24,40,.06),0 8px 18px rgba(16,24,40,.06);--tmd-hd-text:#fff;
  --tmd-font-display:"Manrope",sans-serif;--tmd-font-body:"Inter",sans-serif;--tmd-font-mono:"Inter",sans-serif;
  --tmd-c1:#4F6BED;--tmd-c2:#0FB5A8;--tmd-c3:#E0A100;--tmd-c4:#7A5AF0;--tmd-c5:#E0567A;--tmd-c6:#2BA84A;
}

/* ── Dark-mode variants ──────────────────────────────────────────────────
   Cards stamp data-tm-dark on the host when HA is in dark mode. Each design has
   a light base (above) and a dark override here, so all three follow HA's
   light/dark setting. */
:host([data-tm-design="accessible"]),[data-tm-design="accessible"]{
  /* Colours are the Okabe-Ito colour-blind-safe palette: distinguishable
     under protanopia, deuteranopia and tritanopia. Text is near-black on
     white (~19:1, comfortably past WCAG AAA) and borders are heavy enough to
     carry meaning without relying on hue. Atkinson Hyperlegible is drawn for
     low vision — its letterforms stay distinct where similar glyphs (I/l/1,
     O/0) usually collapse. */
  --tmd-bg:#FFFFFF;--tmd-surface:#FFFFFF;--tmd-surface-2:#F2F2F2;--tmd-border:#111111;
  --tmd-text:#111111;--tmd-dim:#3D3D3D;
  --tmd-accent:#0072B2;--tmd-accent2:#D55E00;--tmd-good:#009E73;--tmd-warn:#E69F00;--tmd-bad:#D55E00;--tmd-gold:#E69F00;
  --tmd-radius:10px;--tmd-radius-sm:6px;--tmd-shadow:0 0 0 2px #111111;--tmd-hd-text:#fff;
  --tmd-font-display:"Atkinson Hyperlegible",sans-serif;
  --tmd-font-body:"Atkinson Hyperlegible",sans-serif;
  --tmd-font-mono:"Atkinson Hyperlegible",monospace;
  --tmd-c1:#0072B2;--tmd-c2:#D55E00;--tmd-c3:#009E73;--tmd-c4:#CC79A7;--tmd-c5:#56B4E9;--tmd-c6:#E69F00;
}
:host([data-tm-design="console"][data-tm-dark]),[data-tm-design="console"][data-tm-dark]{
  --tmd-bg:#0E1320;--tmd-surface:#161D2E;--tmd-surface-2:#1E2740;--tmd-border:#2A3550;
  --tmd-text:#EAF0FF;--tmd-dim:#8A97B8;
  --tmd-accent:#3DDBFF;--tmd-accent2:#FF4D9D;--tmd-good:#4BE08B;--tmd-warn:#FFCB45;--tmd-bad:#FF5A7A;--tmd-gold:#FFCB45;
  --tmd-shadow:0 10px 28px rgba(0,0,0,.5);
  --tmd-c1:#3DDBFF;--tmd-c2:#FF4D9D;--tmd-c3:#FFCB45;--tmd-c4:#7C5CFF;--tmd-c5:#4BE08B;--tmd-c6:#FF8A3D;
}
:host([data-tm-design="playroom"][data-tm-dark]),[data-tm-design="playroom"][data-tm-dark]{
  --tmd-bg:#241A2B;--tmd-surface:#2C2235;--tmd-surface-2:#382B43;--tmd-border:#473754;
  --tmd-text:#F6ECEF;--tmd-dim:#B6A3B4;--tmd-accent:#A88BFF;
  --tmd-shadow:0 10px 24px rgba(0,0,0,.45);
}
:host([data-tm-design="cleanpro"][data-tm-dark]),[data-tm-design="cleanpro"][data-tm-dark]{
  --tmd-bg:#0F141A;--tmd-surface:#181D25;--tmd-surface-2:#212733;--tmd-border:#2C3340;
  --tmd-text:#E6EAF1;--tmd-dim:#97A1B0;--tmd-accent:#6E86F5;--tmd-accent2:#1FC7B8;
  --tmd-shadow:0 1px 2px rgba(0,0,0,.4),0 8px 18px rgba(0,0,0,.35);
}
:host([data-tm-design="accessible"][data-tm-dark]),[data-tm-design="accessible"][data-tm-dark]{
  /* Pure black would bloom on OLED and is harsh for astigmatism; #0A0A0A with
     near-white text still clears AAA. Hues are the Okabe-Ito light variants,
     which stay distinguishable against a dark ground. */
  --tmd-bg:#0A0A0A;--tmd-surface:#0A0A0A;--tmd-surface-2:#1C1C1C;--tmd-border:#F5F5F5;
  --tmd-text:#F5F5F5;--tmd-dim:#CFCFCF;
  --tmd-accent:#56B4E9;--tmd-accent2:#E69F00;--tmd-good:#009E73;--tmd-warn:#F0E442;--tmd-bad:#E69F00;--tmd-gold:#F0E442;
  --tmd-shadow:0 0 0 2px #F5F5F5;
  --tmd-c1:#56B4E9;--tmd-c2:#E69F00;--tmd-c3:#009E73;--tmd-c4:#CC79A7;--tmd-c5:#F0E442;--tmd-c6:#0072B2;
}`;

  // Shared component kit, themed by the tokens above. Every designed card
  // consumes these via __taskmate_design.styles() and adds only its own layout
  // classes. Headers stay FULL-COLOUR (var(--hd)) in every style per house rule.
  const KIT = `
.tmd{overflow:hidden;font-family:var(--tmd-font-body);color:var(--tmd-text);background:var(--tmd-surface);border-radius:var(--tmd-radius);box-shadow:var(--tmd-shadow)}
.tmd-hd{display:flex;align-items:center;gap:11px;padding:13px 15px;background:var(--hd,var(--tmd-accent));color:#fff}
:host([data-tm-design="playroom"]) .tmd-hd{background:linear-gradient(135deg,var(--hd,var(--tmd-accent)),color-mix(in srgb,var(--hd,var(--tmd-accent)) 72%,#fff))}
.tmd-hd .ic{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;background:rgba(255,255,255,.22);font-size:17px;flex:none}
.tmd-hd .tt{font-family:var(--tmd-font-display);font-weight:800;font-size:16px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tmd-hd .tt small{display:block;font-family:var(--tmd-font-body);font-weight:600;font-size:11px;opacity:.85;margin-top:1px}
.tmd-hd .pill{margin-left:auto;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;background:rgba(255,255,255,.25);text-transform:capitalize}
.tmd-hd .cnt{margin-left:auto;min-width:22px;height:22px;padding:0 6px;border-radius:999px;background:var(--tmd-bad);color:#fff;font-size:12px;font-weight:800;display:grid;place-items:center}
.tmd-bd{padding:15px}
.tmd-empty{text-align:center;padding:26px 12px;color:var(--tmd-dim);font-size:.9rem}
.av{width:var(--av,42px);height:var(--av,42px);border-radius:50%;flex:none;display:grid;place-items:center;font-family:var(--tmd-font-display);font-weight:800;color:#fff;font-size:calc(var(--av,42px)*.4);line-height:1;text-align:center;background:var(--ac,var(--tmd-accent));box-shadow:0 2px 6px rgba(0,0,0,.18);overflow:hidden}
:host([data-tm-design="console"]) .av{box-shadow:0 0 0 1px color-mix(in srgb,var(--ac,var(--tmd-accent)) 70%,transparent),0 0 14px color-mix(in srgb,var(--ac,var(--tmd-accent)) 35%,transparent)}
.av ha-icon{--mdc-icon-size:calc(var(--av,42px)*.55);color:#fff}
.av img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.chip{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:700;padding:4px 9px;border-radius:999px;background:var(--tmd-surface-2);color:var(--tmd-text);border:1px solid var(--tmd-border)}
.chip.soft{background:color-mix(in srgb,var(--tmd-accent) 14%,transparent);border-color:transparent;color:var(--tmd-accent)}
.num{font-family:var(--tmd-font-mono);font-weight:800;letter-spacing:-.01em}
.big{font-family:var(--tmd-font-display);font-weight:800;line-height:1}
.bar{height:10px;border-radius:999px;background:var(--tmd-surface-2);overflow:hidden;border:1px solid var(--tmd-border)}
.bar>i{display:block;height:100%;border-radius:999px;background:var(--tmd-accent)}
:host([data-tm-design="console"]) .bar>i{background:linear-gradient(90deg,var(--tmd-accent),var(--tmd-accent2));box-shadow:0 0 12px color-mix(in srgb,var(--tmd-accent) 60%,transparent)}
.row{display:flex;align-items:center;gap:10px}
.muted{color:var(--tmd-dim)}
.divide{height:1px;background:var(--tmd-border);margin:12px 0}
.lead{color:var(--tmd-accent)}
.lead-dot{color:var(--tmd-gold)}
.btn{font:inherit;font-family:var(--tmd-font-display);font-weight:800;font-size:13.5px;line-height:1;border:0;cursor:pointer;padding:9px 14px;border-radius:var(--tmd-radius-sm);background:var(--tmd-accent);color:#fff;display:inline-flex;align-items:center;justify-content:center;gap:6px}
:host([data-tm-design="console"]) .btn{color:#06101c;background:linear-gradient(135deg,var(--tmd-accent),color-mix(in srgb,var(--tmd-accent) 60%,var(--tmd-accent2)));box-shadow:0 0 14px color-mix(in srgb,var(--tmd-accent) 45%,transparent)}
.btn.ghost{background:var(--tmd-surface-2);color:var(--tmd-text);border:1px solid var(--tmd-border)}
:host([data-tm-design="console"]) .btn.ghost{box-shadow:none}
.btn.good{background:var(--tmd-good);color:#06301f}
.btn.bad{background:var(--tmd-bad);color:#3a0d0d}
.btn.round{border-radius:50%;width:38px;height:38px;padding:0;justify-content:center;font-size:18px}
.btn.sm{padding:6px 10px;font-size:12.5px}
.stat{background:var(--tmd-surface-2);border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm);padding:10px 11px}
.stat .k{font-size:11px;font-weight:700;color:var(--tmd-dim);text-transform:uppercase;letter-spacing:.04em}
.stat .v{font-family:var(--tmd-font-display);font-weight:800;font-size:20px;margin-top:2px}
.grid{display:grid;gap:10px}`;

  if (!document.getElementById("taskmate-design-fonts")) {
    const styleEl = document.createElement("style");
    styleEl.id = "taskmate-design-fonts";
    styleEl.textContent = FONT_IMPORT;
    document.head.appendChild(styleEl);
  }

  /**
   * The design tokens as a Lit CSSResult, for a card to include in its own
   * `static get styles()` so the :host token rules live inside the card's
   * shadow root (where they actually apply). Built lazily so the Lit `css`
   * tag — borrowed from HA's own card base — is resolved after HA has loaded.
   */
  let _tokenStyles = null;
  function styles() {
    if (_tokenStyles) return _tokenStyles;
    const base = customElements.get("hui-masonry-view") || customElements.get("hui-view");
    if (!base) return null;
    const css = Object.getPrototypeOf(base).prototype.css;
    if (!css) return null;
    const cssText = TOKENS + "\n" + KIT;
    const arr = [cssText];
    arr.raw = [cssText]; // satisfy css() implementations that read strings.raw
    try { _tokenStyles = css(arr); } catch (_e) { _tokenStyles = null; }
    return _tokenStyles;
  }

  /**
   * Global default design. Read the card's own entity first (the overview
   * sensor exposes card_design at top level); otherwise scan TaskMate sensors
   * so the default resolves even for cards bound to child-specific entities.
   */
  function _globalDesign(hass, entity) {
    if (!hass || !hass.states) return "classic";
    const own = hass.states[entity] && hass.states[entity].attributes;
    if (own && IDS.includes(own.card_design)) return own.card_design;
    for (const eid in hass.states) {
      if (eid.indexOf("sensor.taskmate") !== 0) continue;
      const a = hass.states[eid].attributes;
      if (a && IDS.includes(a.card_design)) return a.card_design;
    }
    return "classic";
  }

  /**
   * Resolve the active design for a card.
   * Precedence: per-card config.card_design → global default → classic.
   */
  function resolve(hass, config, entity) {
    const perCard = config && config.card_design;
    if (perCard && perCard !== "global" && IDS.includes(perCard)) return perCard;
    return _globalDesign(hass, entity);
  }

  // Relative luminance (0..1) of a CSS colour string, or null if unparseable.
  function _luminance(c) {
    if (!c) return null;
    c = c.trim();
    let r, g, b;
    let m = c.match(/^#([0-9a-f]{3})$/i);
    if (m) { const h = m[1]; r = parseInt(h[0] + h[0], 16); g = parseInt(h[1] + h[1], 16); b = parseInt(h[2] + h[2], 16); }
    else if ((m = c.match(/^#([0-9a-f]{6})$/i))) { r = parseInt(m[1].slice(0, 2), 16); g = parseInt(m[1].slice(2, 4), 16); b = parseInt(m[1].slice(4, 6), 16); }
    else if ((m = c.match(/rgba?\(\s*([\d.]+)[ ,]+([\d.]+)[ ,]+([\d.]+)/i))) { r = +m[1]; g = +m[2]; b = +m[3]; }
    else return null;
    return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
  }

  /**
   * Is the card currently in dark mode? Order:
   * 1. an explicitly-selected dark theme (hass.themes.darkMode)
   * 2. the ACTUAL rendered background brightness (covers the default theme
   *    following the OS, custom themes, etc.) read from the card's own scope
   * 3. the OS preference as a last resort.
   * `el` is the card host; its inherited --card/--primary-background-color
   * reflects whatever theme is live around it.
   */
  function isDark(hass, el) {
    if (hass && hass.themes && hass.themes.darkMode) return true;
    if (el && typeof getComputedStyle === "function") {
      try {
        const cs = getComputedStyle(el);
        const bg = (cs.getPropertyValue("--card-background-color")
          || cs.getPropertyValue("--primary-background-color")
          || cs.backgroundColor || "").trim();
        const lum = _luminance(bg);
        if (lum !== null) return lum < 0.5;
      } catch (_e) { /* ignore */ }
    }
    return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
  }

  /**
   * Resolve the design AND stamp the host element: data-tm-design + (in dark
   * mode) data-tm-dark. Cards call this once at the top of render() and branch
   * on the returned id. Returns "classic" when no design applies.
   */
  function apply(el, hass, config, entity) {
    const design = resolve(hass, config, entity);
    if (el) {
      el.setAttribute("data-tm-design", design);
      el.toggleAttribute("data-tm-dark", design !== "classic" && isDark(hass, el));
    }
    return design;
  }

  /** ha-form select options for a per-card design override (includes "use global"). */
  function editorOptions(t) {
    const label = (k, fallback) => (t ? t("common.design." + k) : fallback);
    return [
      { value: "global",   label: label("use_global", "Use global default") },
      { value: "classic",  label: label("classic", "Classic") },
      { value: "playroom", label: label("playroom", "Playroom") },
      { value: "console",  label: label("console", "Console") },
      { value: "cleanpro", label: label("cleanpro", "Clean Pro") },
      { value: "accessible", label: label("accessible", "Accessible") },
    ];
  }

  // Raw token + kit CSS for non-Lit consumers (e.g. the admin panel, which
  // renders to light DOM). Inject into a <style> and set data-tm-design on a
  // wrapper element; the [data-tm-design] selector variant applies there.
  const cssText = TOKENS + "\n" + KIT;

  // ── Shared header-colour picker (QUAL-3) ───────────────────────────────
  // The same ~30-line colour-picker editor block was copy-pasted into 17 card
  // editors. They render it via a thin `_renderColourPicker` wrapper that calls
  // this helper, passing their own update method as onInput/onPreset/onReset.
  const _DEFAULT_COLOUR_PRESETS = ["#e67e22", "#27ae60", "#9b59b6", "#f1c40f", "#e74c3c", "#34495e"];

  function colourPicker(opts) {
    const base = customElements.get("hui-masonry-view") || customElements.get("hui-view");
    const html = base ? Object.getPrototypeOf(base).prototype.html : null;
    if (!html) return "";
    const { defaultValue, current, label, helper, resetLabel, onInput, onPreset, onReset } = opts;
    const presets = [defaultValue, ...(opts.presets || _DEFAULT_COLOUR_PRESETS)];
    const isActive = (c) => String(c).toLowerCase() === String(current).toLowerCase();
    return html`
      <div class="colour-field">
        <span class="colour-field-label">${label}</span>
        <div class="colour-field-body">
          <label class="colour-swatch-wrapper">
            <input type="color" .value=${current} @input=${(e) => onInput(e.target.value)} />
            <span class="colour-swatch-preview" style="background:${current}"></span>
          </label>
          <span class="colour-hex">${current}</span>
          <div class="colour-presets">
            ${presets.map((p) => html`
              <button class="preset-swatch ${isActive(p) ? "active" : ""}"
                style="background:${p}" title=${p}
                @click=${(e) => { e.preventDefault(); onPreset(p); }}></button>
            `)}
          </div>
          <button class="colour-reset"
            @click=${(e) => { e.preventDefault(); onReset(); }}>${resetLabel}</button>
        </div>
        <div class="colour-helper">${helper}</div>
      </div>
    `;
  }

  window.__taskmate_design = { IDS, resolve, isDark, apply, editorOptions, styles, cssText, tokensCSS: TOKENS, colourPicker };

  /**
   * The single place that decides what a chore looks like (#750).
   *
   * Precedence: uploaded picture → MDI icon → the call site's own fallback.
   *
   * Returns a plain descriptor rather than a template, for two reasons: this
   * file has no ES-module imports and no Lit dependency and must keep it that
   * way, and the `icon` case is not uniform across sites — the reorder card
   * renders chore.icon as an <ha-icon> only when it starts with "mdi:" and as
   * raw text otherwise, because the field can hold an emoji. A descriptor lets
   * each site keep its existing icon and fallback handling untouched.
   */
  window.__taskmate_chore_visual = window.__taskmate_chore_visual || function (chore) {
    if (chore && chore.image_url) return { kind: "image", url: chore.image_url };
    if (chore && chore.icon) return { kind: "icon", icon: chore.icon };
    return { kind: "none" };
  };

  // ── Card-picker entity suggestions (HA 2026.6+) ────────────────────────────
  // Custom cards may add getEntitySuggestion(hass, entityId) to their
  // window.customCards entry; when a user picks an entity in the card picker,
  // HA shows matching cards in a "Community" section. This shared helper lets
  // every TaskMate card opt in with a one-liner. We identify a TaskMate sensor
  // by its attribute SIGNATURE rather than its entity_id, so suggestions still
  // work after an entity_id rename and across multi-instance setups (e.g.
  // sensor.taskmate_overview_2). getEntitySuggestion runs at pick-time, by
  // which point every card module (and getStubConfig) is defined — so reading
  // this global from a card's registration closure is load-order safe.
  //
  //   role "overview" — the hub sensor (sensor.taskmate_overview): carries
  //                     points_name + the children[] summary. Almost every card
  //                     reads from it.
  //   role "activity" — the activity sensor (sensor.taskmate_activity):
  //                     recent_completions[] + photo_gallery. Used by the photo
  //                     gallery card.
  window.__taskmate_suggest = window.__taskmate_suggest || function (hass, entityId, type, role) {
    if (!entityId || typeof entityId !== "string" || !entityId.startsWith("sensor.")) return null;
    const a = hass && hass.states && hass.states[entityId] && hass.states[entityId].attributes;
    if (!a) return null;
    const isOverview = a.points_name !== undefined && Array.isArray(a.children);
    const isActivity = Array.isArray(a.recent_completions) && a.photo_gallery !== undefined;
    if (!(role === "activity" ? isActivity : isOverview)) return null;
    // Mirror the card's own stub defaults (mode/title/days/…) so a suggested
    // card lands configured exactly as if added manually, just on this entity.
    let stub = {};
    try {
      const el = customElements.get(type);
      if (el && typeof el.getStubConfig === "function") stub = el.getStubConfig() || {};
    } catch (e) { stub = {}; }
    return { config: Object.assign({}, stub, { type: "custom:" + type, entity: entityId }) };
  };

  // ── Chore-photo lightbox ───────────────────────────────────────────────────
  // A single shared, design-AGNOSTIC image viewer for evidence photos. It is
  // mounted on document.body (NOT inside any card's shadow root), so it paints
  // above every card, the admin panel, and HA's own stacking contexts no matter
  // how deeply the trigger is nested or whether an ancestor clips overflow.
  // Callers pass an ALREADY safety-checked URL (tmSafePhotoUrl/_safePhotoUrl),
  // an optional caption string, and an optional localised close label. Closes on
  // backdrop click, the ✕ button, or Escape. Deliberately uses no --tmd-* tokens
  // so it looks right under all four design styles.
  window.__taskmate_lightbox = window.__taskmate_lightbox || (function () {
    let overlay = null, imgEl = null, capEl = null, closeBtn = null, lastFocus = null;

    function onKey(e) { if (e.key === "Escape") { e.stopPropagation(); close(); } }

    function close() {
      if (!overlay) return;
      overlay.classList.remove("open");
      document.body.style.overflow = "";
      document.removeEventListener("keydown", onKey, true);
      window.setTimeout(() => { if (overlay) overlay.style.display = "none"; if (imgEl) imgEl.src = ""; }, 180);
      if (lastFocus && typeof lastFocus.focus === "function") { try { lastFocus.focus(); } catch (e) {} }
      lastFocus = null;
    }

    function build() {
      overlay = document.createElement("div");
      overlay.className = "tm-lightbox";
      overlay.setAttribute("role", "dialog");
      overlay.setAttribute("aria-modal", "true");
      overlay.innerHTML =
        "<style>" +
        ".tm-lightbox{position:fixed;inset:0;z-index:2147483000;display:none;flex-direction:column;" +
        "align-items:center;justify-content:center;gap:14px;box-sizing:border-box;" +
        "padding:max(16px,env(safe-area-inset-top)) 16px max(16px,env(safe-area-inset-bottom));" +
        "background:rgba(8,10,14,.86);opacity:0;transition:opacity .18s ease;" +
        "font-family:'Inter',system-ui,-apple-system,'Segoe UI',sans-serif;}" +
        ".tm-lightbox.open{opacity:1;}" +
        ".tm-lightbox img{max-width:92vw;max-height:80vh;object-fit:contain;border-radius:10px;" +
        "background:#111;box-shadow:0 18px 60px rgba(0,0,0,.6);}" +
        ".tm-lightbox .tm-lb-cap{color:#f4f6fb;font-size:14px;font-weight:600;text-align:center;" +
        "max-width:92vw;line-height:1.4;text-shadow:0 1px 2px rgba(0,0,0,.6);}" +
        ".tm-lightbox .tm-lb-close{position:absolute;top:14px;right:16px;width:42px;height:42px;border:none;" +
        "border-radius:50%;background:rgba(255,255,255,.14);color:#fff;font-size:22px;line-height:1;" +
        "cursor:pointer;transition:background .15s;}" +
        ".tm-lightbox .tm-lb-close:hover{background:rgba(255,255,255,.28);}" +
        "@media (prefers-reduced-motion:reduce){.tm-lightbox{transition:none;}}" +
        "</style>" +
        "<button class=\"tm-lb-close\" type=\"button\">✕</button>" +
        "<img alt=\"\">" +
        "<div class=\"tm-lb-cap\"></div>";
      document.body.appendChild(overlay);
      imgEl = overlay.querySelector("img");
      capEl = overlay.querySelector(".tm-lb-cap");
      closeBtn = overlay.querySelector(".tm-lb-close");
      closeBtn.addEventListener("click", close);
      // Only a click on the dimmed backdrop itself (not image/caption/button) closes.
      overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
    }

    return function (src, caption, closeLabel) {
      if (!src) return;
      if (!overlay) build();
      lastFocus = document.activeElement;
      imgEl.src = src;
      if (caption) { capEl.textContent = caption; capEl.style.display = ""; }
      else { capEl.textContent = ""; capEl.style.display = "none"; }
      closeBtn.setAttribute("aria-label", closeLabel || "Close");
      closeBtn.title = closeLabel || "Close";
      overlay.style.display = "flex";
      void overlay.offsetWidth; // force reflow so the opacity transition runs from 0
      overlay.classList.add("open");
      document.body.style.overflow = "hidden";
      document.addEventListener("keydown", onKey, true);
      closeBtn.focus();
    };
  })();
})();
