/**
 * TaskMate Rewards Card
 * A Lovelace card for displaying available rewards.
 * Shows rewards in a vertical list with star cost, name, description, and progress gauges.
 * Supports regular rewards, Jackpot rewards, and (v3.0+) Pool Mode "savings jar" rewards.
 *
 * Version: 0.1.3
 * Last Updated: 2026-04-17
 */

const LitElement = customElements.get("hui-masonry-view")
  ? Object.getPrototypeOf(customElements.get("hui-masonry-view"))
  : Object.getPrototypeOf(customElements.get("hui-view"));

const html = LitElement.prototype.html;
const css = LitElement.prototype.css;

const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);

class TaskMateRewardsCard extends LitElement {
  static get properties() {
    return {
      hass: { type: Object },
      config: { type: Object },
      _loading: { type: Object },
      _selectedChildId: { type: String },
      _pendingClaim: { type: Object },
    };
  }

  shouldUpdate(changedProps) {
    if (changedProps.has("hass")) {
      return window.__taskmate_hasChanged
        ? window.__taskmate_hasChanged(changedProps.get("hass"), this.hass, this.config?.entity)
        : true;
    }
    return true;
  }

  constructor() {
    super();
    this._loading = {};
    this._selectedChildId = null;
    this._pendingClaim = null;
  }

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

  static get styles() {
    const base = css`
      :host {
        display: block;
        --reward-purple: #9b59b6;
        --reward-purple-light: #a569bd;
        --reward-gold: #f1c40f;
        --reward-gold-dark: #d4a80a;
        --text-primary: var(--primary-text-color, #212121);
        --text-secondary: var(--secondary-text-color, #757575);
        --card-bg: var(--card-background-color, #fff);
        --divider: var(--divider-color, #e0e0e0);
      }

      ha-card {
        overflow: hidden;
      }

      .card-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 14px 18px;
        background: var(--taskmate-header-bg, #e67e22);
        color: white;
      }

      .header-content {
        display: flex;
        align-items: center;
        gap: 12px;
      }

      .header-icon {
        --mdc-icon-size: 32px;
        opacity: 0.95;
      }

      .header-title {
        font-size: 1.3rem;
        font-weight: 600;
      }

      .reward-count {
        background: rgba(255, 255, 255, 0.2);
        padding: 4px 12px;
        border-radius: 16px;
        font-size: 0.9rem;
        font-weight: 500;
      }

      /* ── Child tabs (shown when no child_id is pinned and 2+ children) ── */
      .child-tabs {
        display: flex;
        gap: 6px;
        padding: 12px 16px 0;
        overflow-x: auto;
        scrollbar-width: none;
      }
      .child-tabs::-webkit-scrollbar { display: none; }
      .child-tab {
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 6px 14px;
        border-radius: 20px;
        background: var(--divider, rgba(0, 0, 0, 0.08));
        border: 2px solid transparent;
        cursor: pointer;
        font-size: 0.9rem;
        font-weight: 500;
        white-space: nowrap;
        color: var(--text-secondary, #666);
        transition: all 0.15s;
      }
      .child-tab ha-icon { --mdc-icon-size: 18px; }
      .child-tab.selected {
        background: color-mix(in srgb, var(--taskmate-header-bg, #e67e22) 16%, transparent);
        border-color: var(--taskmate-header-bg, #e67e22);
        color: var(--taskmate-header-bg, #e67e22);
      }

      .card-content {
        padding: 16px;
        display: flex;
        flex-direction: column;
        gap: 12px;
      }

      /* Individual reward row */
      .reward-row {
        display: flex;
        align-items: flex-start;
        gap: 16px;
        padding: 16px;
        background: var(--card-bg);
        border: 1px solid var(--divider);
        border-radius: 12px;
        transition: box-shadow 0.2s ease, transform 0.15s ease;
      }

      .reward-row:hover {
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        transform: translateY(-1px);
      }

      /* Cost badge - prominently displayed */
      .cost-badge {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        min-width: 70px;
        padding: 12px 8px;
        background: linear-gradient(135deg, var(--reward-gold) 0%, var(--reward-gold-dark) 100%);
        border-radius: 12px;
        box-shadow: 0 2px 8px rgba(241, 196, 15, 0.3);
        flex-shrink: 0;
      }

      .cost-badge ha-icon {
        --mdc-icon-size: 24px;
        color: white;
        filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
        margin-bottom: 4px;
      }

      .cost-value {
        font-size: 1.4rem;
        font-weight: 700;
        color: white;
        text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
        line-height: 1;
      }

      .cost-label {
        font-size: 0.65rem;
        font-weight: 600;
        color: rgba(255, 255, 255, 0.9);
        text-transform: uppercase;
        letter-spacing: 0.5px;
        margin-top: 2px;
      }

      /* Reward details */
      .reward-details {
        display: flex;
        flex-direction: column;
        gap: 6px;
        flex: 1;
        min-width: 0;
        padding-top: 4px;
      }

      .reward-name {
        font-size: 1.15rem;
        font-weight: 600;
        color: var(--text-primary);
        line-height: 1.3;
      }

      .reward-description {
        font-size: 0.9rem;
        color: var(--text-secondary);
        line-height: 1.4;
      }

      /* Child assignment indicator */
      .assigned-children {
        display: flex;
        flex-wrap: wrap;
        gap: 4px;
        margin-top: 4px;
      }

      .child-badge {
        display: inline-flex;
        align-items: center;
        padding: 2px 8px;
        background: rgba(155, 89, 182, 0.15);
        border-radius: 12px;
        font-size: 0.75rem;
        color: var(--reward-purple);
        font-weight: 500;
      }

      .child-badge.all-children {
        background: rgba(46, 204, 113, 0.15);
        color: #27ae60;
      }

      /* Progress bar styles */
      .progress-section {
        display: flex;
        flex-direction: column;
        gap: 6px;
        margin-top: 8px;
        width: 100%;
      }

      .progress-bar-container {
        display: flex;
        align-items: center;
        gap: 10px;
        width: 100%;
      }

      .progress-bar {
        flex: 1;
        height: 14px;
        background: var(--divider-color, #e0e0e0);
        border-radius: 7px;
        overflow: hidden;
        position: relative;
        border: 2px solid rgba(52, 152, 219, 0.4);
        box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
      }

      .progress-fill {
        height: 100%;
        border-radius: 7px;
        background: linear-gradient(90deg, #3498db 0%, #2ecc71 100%);
        transition: width 0.4s ease;
        position: relative;
        overflow: hidden;
      }

      .progress-fill::after {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 50%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.4),
          transparent
        );
        animation: shimmer 2s infinite;
      }

      @keyframes shimmer {
        0% { left: -100%; }
        100% { left: 200%; }
      }

      .progress-text {
        font-size: 0.85rem;
        font-weight: 600;
        color: var(--text-secondary);
        white-space: nowrap;
        min-width: 0;
        text-align: right;
      }

      /* Jackpot reward styles */
      .reward-row.jackpot {
        border: 2px solid var(--reward-gold);
        background: linear-gradient(135deg, rgba(241, 196, 15, 0.08) 0%, rgba(255, 255, 255, 0) 100%);
        position: relative;
        overflow: hidden;
      }

      .reward-row.jackpot::before {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: linear-gradient(
          45deg,
          transparent 40%,
          rgba(241, 196, 15, 0.1) 50%,
          transparent 60%
        );
        animation: jackpot-shine 40s ease-in-out infinite;
        pointer-events: none;
      }

      /* Shine 3 times (each ~2.5s = 7.5s total), then wait ~32.5s before repeating */
      @keyframes jackpot-shine {
        0% { transform: translateX(-100%); }
        2.5% { transform: translateX(100%); }
        5% { transform: translateX(-100%); }
        7.5% { transform: translateX(100%); }
        10% { transform: translateX(-100%); }
        12.5% { transform: translateX(100%); }
        15%, 100% { transform: translateX(-100%); opacity: 0; }
      }

      .jackpot-label {
        display: inline-flex;
        align-items: center;
        gap: 4px;
        padding: 3px 10px;
        background: linear-gradient(135deg, var(--reward-gold) 0%, #e67e22 100%);
        border-radius: 12px;
        font-size: 0.7rem;
        font-weight: 700;
        color: white;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        margin-bottom: 4px;
        box-shadow: 0 2px 6px rgba(241, 196, 15, 0.4);
      }

      .jackpot-label span {
        font-size: 0.8rem;
      }

      /* Multi-child progress bar for jackpot */
      .jackpot-progress-bar {
        flex: 1;
        height: 18px;
        background: var(--divider-color, #e0e0e0);
        border-radius: 9px;
        overflow: hidden;
        position: relative;
        display: flex;
        border: 2px solid rgba(241, 196, 15, 0.5);
        box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
      }

      .jackpot-segment {
        height: 100%;
        transition: width 0.4s ease;
        position: relative;
        overflow: hidden;
      }

      .jackpot-segment::after {
        content: '';
        position: absolute;
        top: 0;
        left: -100%;
        width: 100%;
        height: 100%;
        background: linear-gradient(
          90deg,
          transparent,
          rgba(255, 255, 255, 0.4),
          transparent
        );
        animation: jackpot-segment-shimmer 40s infinite;
      }

      /* Shimmer 3 times then wait ~30s before repeating */
      @keyframes jackpot-segment-shimmer {
        0% { left: -100%; }
        2.5% { left: 200%; }
        5% { left: -100%; }
        7.5% { left: 200%; }
        10% { left: -100%; }
        12.5% { left: 200%; }
        15%, 100% { left: -100%; opacity: 0; }
      }

      .jackpot-segment:first-child {
        border-radius: 9px 0 0 9px;
      }

      .jackpot-segment:last-child {
        border-radius: 0 9px 9px 0;
      }

      .jackpot-segment:only-child {
        border-radius: 9px;
      }

      /* Fun kid-friendly colors for segments */
      .jackpot-segment.color-0 { background: linear-gradient(180deg, #ff6b9d 0%, #e91e63 100%); }
      .jackpot-segment.color-1 { background: linear-gradient(180deg, #64b5f6 0%, #2196f3 100%); }
      .jackpot-segment.color-2 { background: linear-gradient(180deg, #81c784 0%, #4caf50 100%); }
      .jackpot-segment.color-3 { background: linear-gradient(180deg, #ffb74d 0%, #ff9800 100%); }
      .jackpot-segment.color-4 { background: linear-gradient(180deg, #ba68c8 0%, #9c27b0 100%); }
      .jackpot-segment.color-5 { background: linear-gradient(180deg, #4dd0e1 0%, #00bcd4 100%); }

      .jackpot-breakdown {
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
        margin-top: 4px;
        font-size: 0.8rem;
        color: var(--text-secondary);
      }

      .jackpot-child-contribution {
        display: inline-flex;
        align-items: center;
        gap: 3px;
        padding: 2px 8px;
        background: color-mix(in srgb, var(--primary-text-color, #212121) 8%, transparent);
        border-radius: 10px;
        font-weight: 500;
      }

      .jackpot-child-contribution .color-dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        flex-shrink: 0;
      }

      .jackpot-child-contribution ha-icon {
        --mdc-icon-size: 13px;
        vertical-align: middle;
        position: relative;
        top: -1px;
      }

      .jackpot-child-contribution .color-dot.color-0 { background: #e91e63; }
      .jackpot-child-contribution .color-dot.color-1 { background: #2196f3; }
      .jackpot-child-contribution .color-dot.color-2 { background: #4caf50; }
      .jackpot-child-contribution .color-dot.color-3 { background: #ff9800; }
      .jackpot-child-contribution .color-dot.color-4 { background: #9c27b0; }
      .jackpot-child-contribution .color-dot.color-5 { background: #00bcd4; }

      .jackpot-total {
        font-weight: 700;
        color: var(--reward-gold-dark);
        padding: 2px 8px;
        background: rgba(241, 196, 15, 0.15);
        border-radius: 10px;
      }

      /* Cost badge for jackpot - special styling */
      .reward-row.jackpot .cost-badge {
        background: linear-gradient(135deg, var(--reward-gold) 0%, #e67e22 100%);
        box-shadow: 0 3px 10px rgba(241, 196, 15, 0.4);
      }

      /* Reward icon + claim button wrapper */
      /* Child picker shown when no child_id configured */
      .child-picker-row {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 10px 16px;
        background: var(--secondary-background-color, #f5f5f5);
        border-bottom: 1px solid var(--divider-color, #e0e0e0);
      }

      .child-picker-label {
        font-size: 0.82rem;
        font-weight: 600;
        color: var(--secondary-text-color);
        white-space: nowrap;
      }

      .child-picker-select {
        flex: 1;
        padding: 5px 8px;
        border: 1px solid var(--divider-color, #e0e0e0);
        border-radius: 6px;
        background: var(--card-background-color, #fff);
        color: var(--primary-text-color);
        font-size: 0.85rem;
        cursor: pointer;
      }

      .reward-right-col {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
        flex-shrink: 0;
      }

      .reward-icon-container {
        width: 44px;
        height: 44px;
        border-radius: 50%;
        background: linear-gradient(135deg, var(--reward-purple) 0%, var(--reward-purple-light) 100%);
        display: flex;
        align-items: center;
        justify-content: center;
        flex-shrink: 0;
      }

      .reward-icon-container ha-icon {
        --mdc-icon-size: 24px;
        color: white;
      }

      /* Pending approval state */
      .reward-row.pending-approval {
        opacity: 0.6;
        border-left: 3px solid #e67e22;
      }

      .pending-label {
        display: inline-flex;
        align-items: center;
        gap: 4px;
        background: rgba(230,126,34,0.12);
        color: #e67e22;
        border-radius: 8px;
        padding: 3px 8px;
        font-size: 0.75rem;
        font-weight: 600;
        margin-top: 4px;
      }

      .pending-label ha-icon { --mdc-icon-size: 12px; }

      /* Availability badges (low stock / sold out / expiring / expired) */
      .availability-badges { display: inline-flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
      .availability-badge {
        display: inline-flex;
        align-items: center;
        border-radius: 8px;
        padding: 3px 8px;
        font-size: 0.72rem;
        font-weight: 700;
        letter-spacing: 0.02em;
      }
      .availability-badge.badge-sold-out {
        background: rgba(189,195,199,0.25);
        color: var(--secondary-text-color, #757575);
      }
      .availability-badge.badge-expired {
        background: color-mix(in srgb, var(--secondary-text-color, #757575) 18%, transparent);
        color: var(--secondary-text-color, #757575);
      }
      .availability-badge.badge-low-stock {
        background: rgba(231,76,60,0.15);
        color: #c0392b;
      }
      .availability-badge.badge-expiring-soon {
        background: rgba(243,156,18,0.18);
        color: #d35400;
      }
      .reward-row.unavailable { opacity: 0.55; }

      /* Claim button */
      .claim-btn {
        width: 42px; height: 42px;
        border-radius: 50%; border: none; cursor: pointer;
        background: linear-gradient(135deg, #9b59b6, #8e44ad);
        color: white;
        display: flex; align-items: center; justify-content: center;
        box-shadow: 0 3px 10px rgba(155,89,182,0.35);
        transition: transform 0.15s, box-shadow 0.15s;
        flex-shrink: 0;
      }

      .claim-btn:hover { transform: scale(1.08); box-shadow: 0 4px 14px rgba(155,89,182,0.45); }
      .claim-btn:active { transform: scale(0.96); }
      .claim-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
      .claim-btn ha-icon { --mdc-icon-size: 22px; }

      .claim-btn.cant-afford {
        background: linear-gradient(135deg, #bdc3c7, #95a5a6);
        box-shadow: none;
        cursor: not-allowed;
      }

      /* Empty state */
      .empty-state {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 48px 24px;
        color: var(--text-secondary);
        text-align: center;
      }

      .empty-state ha-icon {
        --mdc-icon-size: 56px;
        margin-bottom: 16px;
        opacity: 0.5;
        color: var(--reward-purple);
      }

      .empty-state .message {
        font-size: 1.1rem;
        font-weight: 500;
        margin-bottom: 4px;
        color: var(--text-primary);
      }

      .empty-state .submessage {
        font-size: 0.9rem;
        opacity: 0.8;
      }

      /* Error state */
      .error-state {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 40px 20px;
        color: var(--error-color, #f44336);
        text-align: center;
      }

      .error-state ha-icon {
        --mdc-icon-size: 48px;
        margin-bottom: 16px;
      }

      /* Responsive adjustments */
      @media (max-width: 430px) {
        .card-header {
          padding: 14px 18px;
        }

        .header-title {
          font-size: 1.15rem;
        }

        .card-content {
          padding: 12px;
          gap: 10px;
        }

        .reward-row {
          padding: 12px;
          gap: 12px;
        }

        .cost-badge {
          min-width: 60px;
          padding: 10px 6px;
        }

        .cost-badge ha-icon {
          --mdc-icon-size: 20px;
        }

        .cost-value {
          font-size: 1.2rem;
        }

        .reward-name {
          font-size: 1.05rem;
        }

        .reward-description {
          font-size: 0.85rem;
        }

        .reward-icon-container {
          width: 38px;
          height: 38px;
        }

        .reward-icon-container ha-icon {
          --mdc-icon-size: 20px;
        }
      }

      /* v3.0 Pool Mode styles */
      .spendable-banner {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 10px 14px;
        margin: 8px 14px 0 14px;
        background: linear-gradient(135deg, rgba(52, 152, 219, 0.15), rgba(46, 204, 113, 0.12));
        border-radius: 10px;
        border: 1px solid rgba(52, 152, 219, 0.25);
      }

      .spendable-banner ha-icon {
        color: var(--reward-purple);
        --mdc-icon-size: 22px;
      }

      .spendable-banner .spendable-label {
        color: var(--text-secondary);
        font-size: 0.82rem;
        text-transform: uppercase;
        letter-spacing: 0.04em;
      }

      .spendable-banner .spendable-value {
        font-weight: 700;
        font-size: 1.15rem;
        color: var(--text-primary);
        margin-left: auto;
      }

      .spendable-banner .spendable-of {
        font-weight: 400;
        color: var(--text-secondary);
        font-size: 0.82rem;
        margin-left: 4px;
      }

      /* Pool Mode controls — render BELOW the progress bar, full width */
      .pool-controls {
        display: flex;
        gap: 6px;
        flex-wrap: wrap;
        justify-content: flex-start;
        margin-top: 4px;
      }

      .pool-controls-redeem {
        justify-content: flex-start;
      }

      .alloc-btn {
        flex: 1 1 0;
        min-width: 56px;
        padding: 7px 10px;
        border-radius: 8px;
        border: none;
        cursor: pointer;
        background: linear-gradient(135deg, #3498db, #2980b9);
        color: white;
        font-weight: 700;
        font-size: 0.9rem;
        transition: transform 0.1s, opacity 0.1s, box-shadow 0.15s;
        box-shadow: 0 1px 3px rgba(52, 152, 219, 0.3);
      }

      .alloc-btn:hover:not(:disabled) {
        transform: scale(1.04);
        box-shadow: 0 2px 6px rgba(52, 152, 219, 0.5);
      }

      .alloc-btn:disabled {
        opacity: 0.4;
        cursor: not-allowed;
      }

      /* Custom-amount deposit (#559): sits beside the quick buttons, wraps
         below them on narrow widths. */
      .deposit-custom {
        display: flex;
        align-items: stretch;
        gap: 6px;
        margin-left: auto;
        flex: 0 0 auto;
      }
      .deposit-custom input {
        width: 72px;
        min-width: 0;
        padding: 7px 8px;
        border-radius: 8px;
        border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.18));
        background: var(--card-background-color, #fff);
        color: var(--primary-text-color, #111);
        font-size: 0.9rem;
        font-weight: 600;
        text-align: center;
      }
      .deposit-custom input:disabled {
        opacity: 0.4;
        cursor: not-allowed;
      }
      .deposit-custom .alloc-btn.deposit-add {
        flex: 0 0 auto;
        min-width: 0;
      }
      /* Hide the number spinners for a cleaner look */
      .deposit-custom input::-webkit-outer-spin-button,
      .deposit-custom input::-webkit-inner-spin-button {
        -webkit-appearance: none;
        margin: 0;
      }
      .deposit-custom input[type="number"] { -moz-appearance: textfield; }

      .redeem-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 6px;
        flex: 1 1 auto;
        padding: 9px 18px;
        border-radius: 20px;
        border: none;
        cursor: pointer;
        background: linear-gradient(135deg, #2ecc71, #27ae60);
        color: white;
        font-weight: 700;
        font-size: 0.95rem;
        box-shadow: 0 3px 10px rgba(46, 204, 113, 0.35);
        animation: pulse-green 1.6s ease-in-out infinite;
      }

      .redeem-btn ha-icon {
        --mdc-icon-size: 18px;
      }

      @keyframes pulse-green {
        0%, 100% { box-shadow: 0 3px 10px rgba(46, 204, 113, 0.35); transform: scale(1); }
        50% { box-shadow: 0 3px 18px rgba(46, 204, 113, 0.65); transform: scale(1.02); }
      }

      .pool-locked-note {
        font-size: 0.72rem;
        color: var(--text-secondary);
        font-style: italic;
        margin-top: 4px;
      }

      .confirm-overlay {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.5);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 9999;
      }
      .confirm-dialog {
        background: var(--card-background-color, #fff);
        border-radius: 16px;
        padding: 24px;
        max-width: 320px;
        width: 90%;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
        display: flex;
        flex-direction: column;
        gap: 16px;
      }
      .confirm-dialog .dialog-icon {
        display: flex;
        justify-content: center;
        --mdc-icon-size: 48px;
        color: var(--reward-purple);
      }
      .confirm-dialog .dialog-title {
        font-size: 1.15rem;
        font-weight: 600;
        color: var(--primary-text-color);
        text-align: center;
      }
      .confirm-dialog .dialog-body {
        font-size: 0.9rem;
        color: var(--secondary-text-color);
        text-align: center;
        line-height: 1.5;
      }
      .confirm-dialog .dialog-buttons {
        display: flex;
        gap: 10px;
      }
      .confirm-dialog button {
        flex: 1;
        padding: 10px;
        border: none;
        border-radius: 8px;
        font-size: 0.95rem;
        font-weight: 600;
        cursor: pointer;
        transition: transform 0.15s ease, box-shadow 0.15s ease;
      }
      .confirm-dialog button:hover {
        transform: translateY(-1px);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      }
      .confirm-dialog button:active {
        transform: scale(0.97);
      }
      .confirm-dialog .btn-cancel {
        background: var(--secondary-background-color, #f5f5f5);
        color: var(--primary-text-color);
      }
      .confirm-dialog .btn-confirm {
        background: linear-gradient(135deg, var(--reward-purple) 0%, var(--reward-purple-light) 100%);
        color: white;
      }
      .dialog-points-summary {
        background: var(--secondary-background-color, #f5f5f5);
        border-radius: 8px;
        padding: 10px 14px;
        display: flex;
        flex-direction: column;
        gap: 6px;
        font-size: 0.9rem;
        text-align: left;
      }
      .dialog-points-row {
        display: flex;
        justify-content: space-between;
        align-items: center;
        color: var(--primary-text-color);
      }
      .dialog-points-row.cost {
        color: var(--secondary-text-color);
        border-top: 1px solid var(--divider-color, #e0e0e0);
        padding-top: 6px;
      }
      .dialog-points-row.remaining {
        font-weight: 700;
        border-top: 1px solid var(--divider-color, #e0e0e0);
        padding-top: 6px;
        color: var(--reward-purple);
      }
      .points-val {
        display: flex;
        align-items: center;
        gap: 2px;
        font-weight: 600;
      }

      /* ── Designed (playroom / console / cleanpro) — shared .tmd kit comes
         from taskmate-design.js styles(); only card-specific layout below. ── */
      .rw-list { display: flex; flex-direction: column; gap: 11px; }
      /* #604: default is fluid — the list grows to fit every reward. Opt into a
         fixed 360px scroll region with expand_to_fit: false. */
      .rw-list.rw-scroll { max-height: 360px; overflow-y: auto; }
      .rw-jackpot-label { align-self: flex-start; background: var(--tmd-gold); color: #3a2e26;
                          border-color: transparent; font-weight: 800; text-transform: uppercase;
                          letter-spacing: 0.04em; font-size: 10.5px; margin-bottom: 2px; }
      .rw-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
      .rw-badge { font-size: 11px; padding: 2px 8px; }
      .rw-badge-all { background: color-mix(in srgb, var(--tmd-good) 16%, transparent);
                      color: var(--tmd-good); border-color: transparent; }
      .rw-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 2px; }
      .rw-sel { background: var(--tmd-surface-2); border: 1px solid var(--tmd-border);
                border-radius: var(--tmd-radius-sm); padding: 9px 12px; }
      .rw-sel-name { flex: 1; min-width: 0; font-weight: 800; }
      .rw-card { background: var(--tmd-surface-2); border: 1px solid var(--tmd-border);
                 border-radius: var(--tmd-radius-sm); padding: 13px; display: flex;
                 flex-direction: column; gap: 9px; }
      .rw-card.rw-unavail { opacity: 0.55; }
      .rw-jackpot { border: 2px dashed var(--tmd-accent);
                    background: color-mix(in srgb, var(--tmd-accent) 9%, var(--tmd-surface-2)); }
      .rw-top { align-items: flex-start; }
      .rw-emoji { font-size: 28px; line-height: 1; flex: none; }
      .rw-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
      .rw-name { font-weight: 800; font-size: 15px; }
      .rw-desc { font-size: 12px; }
      .rw-avail { align-self: flex-start; font-size: 11px; }
      .rw-avail-warn { background: color-mix(in srgb, var(--tmd-warn) 16%, transparent);
                       color: var(--tmd-warn); }
      .rw-avail-bad { background: color-mix(in srgb, var(--tmd-bad) 16%, transparent);
                      color: var(--tmd-bad); }
      .rw-cost { flex: none; align-self: flex-start; background: var(--tmd-gold);
                 color: #3a2e26; border-color: transparent; font-weight: 800; }
      .rw-pool-bar { height: 14px; display: flex; gap: 0; padding: 0; }
      .rw-pool-bar > i { border-radius: 0; }
      .rw-pool-foot { flex-wrap: wrap; gap: 6px; }
      .rw-contrib { gap: 5px; font-size: 11px; }
      .rw-foot { gap: 8px; }
      .rw-status { font-size: 12px; }
      .rw-status-good { color: var(--tmd-good); font-weight: 800; }
      .rw-pool-btns { gap: 6px; flex-wrap: wrap; align-items: center; }
      .rw-pool-btns .deposit-custom { margin-left: auto; gap: 6px; }
      .rw-deposit-input {
        width: 70px;
        min-width: 0;
        padding: 7px 8px;
        border-radius: var(--tmd-radius-sm);
        border: 1px solid var(--tmd-border);
        background: var(--tmd-surface);
        color: var(--tmd-text);
        font-size: 13px;
        font-weight: 700;
        text-align: center;
      }
      .rw-deposit-input:disabled { opacity: 0.4; cursor: not-allowed; }
      .rw-deposit-input::-webkit-outer-spin-button,
      .rw-deposit-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
      .rw-deposit-input[type="number"] { -moz-appearance: textfield; }
    `;
    const tokens = window.__taskmate_design && window.__taskmate_design.styles
      ? window.__taskmate_design.styles() : null;
    return tokens ? [tokens, base] : base;
  }

  setConfig(config) {
    if (!config.entity) {
      throw new Error(this._t('rewards.error.entity_required'));
    }
    this.config = {
      title: null,
      child_id: null, // Optional: filter rewards for a specific child
      show_child_badges: true, // Show which children can claim each reward
      enable_pool_mode: false, // v3.0: "savings jar" allocation mode (opt-in per card)
      expand_to_fit: true, // #604: grow the card to fit all rewards (designed styles). false = fixed 360px scroll.
      deposit_amounts: [1, 5, 10], // #559: quick-deposit button amounts for pool/jackpot rewards
      header_color: '#e67e22',
      ...config,
    };
  }

  getCardSize() {
    return 3;
  }

  static getConfigElement() {
    return document.createElement("taskmate-rewards-card-editor");
  }

  static getStubConfig() {
    return {
      entity: "sensor.taskmate_overview",
      title: "Rewards",
      child_id: null,
      show_child_badges: true,
      enable_pool_mode: false,
    };
  }

  willUpdate() {
    // #547: when the card isn't pinned to a child via config.child_id, default the
    // in-card selection to the first child so the claim button renders (and the
    // child tabs have a selection). Without this, childId stays null and no claim
    // button is ever shown, making rewards impossible to claim.
    if (this.hass && this.config && !this.config.child_id && !this._selectedChildId) {
      const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity))
        || this.hass.states?.[this.config.entity]?.attributes || {};
      const kids = attrs.children || [];
      if (kids.length) this._selectedChildId = kids[0].id;
    }
  }

  _selectChild(id) {
    this._selectedChildId = id;
  }

  // Classic child picker — shown only when no child_id is pinned and 2+ children.
  _renderChildTabs(children) {
    if (this.config.child_id || children.length <= 1) return html``;
    const activeId = this._selectedChildId || children[0]?.id;
    return html`
      <div class="child-tabs">
        ${children.map((c) => html`
          <div class="child-tab ${activeId === c.id ? 'selected' : ''}"
               @click=${() => this._selectChild(c.id)}>
            <ha-icon icon="${c.avatar || 'mdi:account-circle'}"></ha-icon>
            ${c.name}
          </div>
        `)}
      </div>
    `;
  }

  // Designed-style child picker (reuses the .btn sm button styling).
  _designChildTabs(children) {
    if (this.config.child_id || children.length <= 1) return html``;
    const activeId = this._selectedChildId || children[0]?.id;
    return html`
      <div class="row rw-tabs">
        ${children.map((c) => html`
          <button class="btn sm ${activeId === c.id ? 'good' : 'ghost'}"
                  @click=${() => this._selectChild(c.id)}>${c.name}</button>
        `)}
      </div>
    `;
  }

  render() {
    if (!this.hass || !this.config) {
      return html``;
    }

    const design = window.__taskmate_design
      ? window.__taskmate_design.apply(this, this.hass, this.config, this.config.entity)
      : "classic";
    if (design !== "classic") return this._renderDesigned(design);

    const entity = this.hass.states[this.config.entity];

    if (!entity) {
      return html`
        <ha-card>
          <div class="error-state">
            <ha-icon icon="mdi:alert-circle"></ha-icon>
            <div>${this._t('common.entity_not_found', { entity: this.config.entity })}</div>
          </div>
        </ha-card>
      `;
    }

    const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
    const allRewards = attrs.rewards || [];
    const children = attrs.children || [];
    const pointsIcon = attrs.points_icon || "mdi:star";
    const pointsName = attrs.points_name || this._t("common.stars");

    // Filter rewards based on child_id if configured
    let rewards = allRewards;
    if (this.config.child_id) {
      rewards = allRewards.filter((reward) => {
        const assignedTo = reward.assigned_to || [];
        // Show reward if assigned_to is empty (available to all) OR contains the child_id
        return assignedTo.length === 0 || assignedTo.includes(this.config.child_id);
      });
    }

    // Sort rewards: jackpot first, then by calculated cost ascending within each group
    const sortedRewards = [...rewards].sort((a, b) => {
      // Jackpot rewards come first
      if (a.is_jackpot && !b.is_jackpot) return -1;
      if (!a.is_jackpot && b.is_jackpot) return 1;
      // Within same type, sort by calculated cost ascending
      const aCost = this._getDisplayCost(a);
      const bCost = this._getDisplayCost(b);
      return aCost - bCost;
    });

    // Create a map of child IDs to names for badge display
    const childMap = {};
    children.forEach((child) => {
      childMap[child.id] = child.name;
    });

    // Card-level override: when true, every reward on this card is treated as a pool reward.
    // Otherwise each reward decides for itself via reward.pool_enabled.
    const cardForcesPool = this.config.enable_pool_mode === true;
    const activeChildId = this.config.child_id || this._selectedChildId;
    const activeChild = activeChildId ? children.find((c) => c.id === activeChildId) : null;
    // Show the spendable banner when any visible reward is in pool mode
    const anyPoolReward = sortedRewards.some(
      (r) => cardForcesPool || r.pool_enabled === true || r.is_jackpot
    );

    return html`
      <ha-card>
        <style>:host { --taskmate-header-bg: ${_safeColor(this.config.header_color, '#e67e22')}; }</style>
        <div class="card-header">
          <div class="header-content">
            <ha-icon class="header-icon" icon="mdi:gift-outline"></ha-icon>
            <span class="header-title">${this.config.title || this._t('rewards.default_title')}</span>
          </div>
          ${rewards.length > 0
            ? html`<span class="reward-count">${rewards.length === 1 ? this._t('rewards.reward_count_singular', { count: rewards.length }) : this._t('rewards.reward_count_plural', { count: rewards.length })}</span>`
            : ""}
        </div>

        ${this._renderChildTabs(children)}

        ${anyPoolReward && activeChild ? this._renderSpendableBanner(activeChild, pointsIcon, pointsName) : ''}

        <div class="card-content">
          ${sortedRewards.length === 0
            ? this._renderEmptyState()
            : sortedRewards.map((reward) => this._renderRewardRow(reward, pointsIcon, pointsName, childMap, children))}
        </div>
      </ha-card>

      ${this._pendingClaim ? html`
        <div class="confirm-overlay" @click="${this._cancelClaim}">
          <div class="confirm-dialog" @click="${(e) => e.stopPropagation()}">
            <ha-icon class="dialog-icon" icon="mdi:gift-outline"></ha-icon>
            <div class="dialog-title">${this._t('rewards.confirm_title')}</div>
            <div class="dialog-body">
              ${(() => {
                const r = this._pendingClaim;
                const child = r._child;
                const cost = r.cost;
                // Pool/jackpot redemptions spend POOLED points, not the claiming
                // child's wallet. Showing the child's balance vs the full cost
                // produced a nonsensical negative "Remaining" (#557). Use the pool
                // total (jackpot: summed across all children; savings-jar: this
                // child's allocation) so the calculation reflects the pooled points.
                const isJackpot = r.is_jackpot === true;
                const isPoolRedeem = isJackpot || r.pool_enabled === true || this.config.enable_pool_mode === true;
                let currentPoints;
                let availableLabel;
                if (isPoolRedeem) {
                  const poolAllocations = r.pool_allocations || {};
                  currentPoints = isJackpot
                    ? (typeof r.jackpot_pool_total === 'number' ? r.jackpot_pool_total : 0)
                    : (child ? (poolAllocations[child.id] || 0) : 0);
                  availableLabel = this._t('rewards.confirm_pool_total');
                } else {
                  currentPoints = child ? (typeof child.spendable_balance === 'number' ? child.spendable_balance : (child.points || 0)) : null;
                  availableLabel = this._t('rewards.confirm_your_points', { name: pointsName });
                }
                const remaining = currentPoints !== null ? currentPoints - cost : null;
                return html`
                  <div style="margin-bottom: 12px;">${this._t('rewards.confirm_body', { name: r.name })}</div>
                  ${currentPoints !== null ? html`
                    <div class="dialog-points-summary">
                      <div class="dialog-points-row">
                        <span>${availableLabel}</span>
                        <span class="points-val">
                          <ha-icon icon="${pointsIcon}" style="--mdc-icon-size:14px;"></ha-icon>
                          ${currentPoints}
                        </span>
                      </div>
                      <div class="dialog-points-row cost">
                        <span>${this._t('rewards.confirm_cost')}</span>
                        <span class="points-val">
                          <ha-icon icon="${pointsIcon}" style="--mdc-icon-size:14px;"></ha-icon>
                          −${cost}
                        </span>
                      </div>
                      <div class="dialog-points-row remaining">
                        <span>${this._t('rewards.confirm_remaining')}</span>
                        <span class="points-val">
                          <ha-icon icon="${pointsIcon}" style="--mdc-icon-size:14px;"></ha-icon>
                          ${remaining}
                        </span>
                      </div>
                    </div>
                  ` : ''}
                `;
              })()}
            </div>
            <div class="dialog-buttons">
              <button class="btn-cancel" @click="${this._cancelClaim}">${this._t('common.cancel')}</button>
              <button class="btn-confirm" @click="${this._confirmClaim}">${this._t('rewards.confirm_claim_btn')}</button>
            </div>
          </div>
        </div>
      ` : ""}
    `;
  }

  _renderSpendableBanner(child, pointsIcon, pointsName) {
    const spendable = typeof child.spendable_balance === 'number' ? child.spendable_balance : (child.points || 0);
    const gross = child.points || 0;
    // Only show "/ gross" when points are reserved by a pending claim.
    const detail = spendable !== gross
      ? html`<span class="spendable-of">/ ${gross} ${pointsName}</span>`
      : html`<span class="spendable-of">${pointsName}</span>`;
    return html`
      <div class="spendable-banner">
        <ha-icon icon="${pointsIcon}"></ha-icon>
        <span class="spendable-label">${this._t('rewards.spendable_balance')}</span>
        <span class="spendable-value">
          ${spendable}
          ${detail}
        </span>
      </div>
    `;
  }

  _renderEmptyState() {
    return html`
      <div class="empty-state">
        <ha-icon icon="mdi:gift-off-outline"></ha-icon>
        <div class="message">${this._t('rewards.empty_title')}</div>
        <div class="submessage">${this._t('rewards.empty_subtitle')}</div>
      </div>
    `;
  }

  _renderRewardRow(reward, pointsIcon, pointsName, childMap, children) {
    const rewardIcon = reward.icon || "mdi:gift";
    const hasDescription = reward.description && reward.description.trim().length > 0;
    const assignedTo = reward.assigned_to || [];
    const showChildBadges = this.config.show_child_badges !== false;
    const isJackpot = reward.is_jackpot || false;
    // Pool mode is enabled per-reward (reward.pool_enabled) OR forced on via card config.
    const cardForcesPool = this.config.enable_pool_mode === true;
    // Jackpot rewards are always pool-mode (#552): everyone deposits into the
    // shared jar and it's redeemed when full. Combined points only work via the
    // pool, so jackpots never show the single-child claim button.
    const enablePoolMode = cardForcesPool || reward.pool_enabled === true || isJackpot;
    // All costs are static — just use reward.cost
    const displayCost = this._getDisplayCost(reward);

    // Get relevant children for this reward
    const relevantChildren = assignedTo.length === 0
      ? children
      : children.filter(c => assignedTo.includes(c.id));

    // Determine active child context
    const childId = this.config?.child_id || this._selectedChildId;
    const relevantChild = childId
      ? children.find(c => c.id === childId)
      : relevantChildren[0];

    // Pool allocations for this reward (v3.0). Falls back to empty dict in wallet mode.
    const poolAllocations = reward.pool_allocations || {};
    const jackpotPoolTotal = typeof reward.jackpot_pool_total === 'number' ? reward.jackpot_pool_total : 0;

    // Calculate progress
    let currentStars = 0;
    let childContributions = [];

    if (isJackpot) {
      const sharePerChild = relevantChildren.length > 0
        ? Math.round(displayCost / relevantChildren.length)
        : displayCost;
      relevantChildren.forEach((child, index) => {
        // In pool mode, show allocated points only; in wallet mode show wallet points
        const points = enablePoolMode
          ? (poolAllocations[child.id] || 0)
          : (child.points || 0);
        const shareOfGoal = relevantChildren.length > 0 ? (100 / relevantChildren.length) : 100;
        const weightedProgress = sharePerChild > 0 ? Math.min((points / sharePerChild) * 100, 100) : 0;

        currentStars += points;
        childContributions.push({
          name: child.name,
          points: points,
          colorIndex: index % 6,
          expectedContribution: sharePerChild,
          weightedProgress: weightedProgress,
          shareOfGoal: shareOfGoal,
        });
      });
    } else {
      if (enablePoolMode) {
        // Pool mode: show this child's allocation for this reward
        currentStars = childId ? (poolAllocations[childId] || 0) : 0;
      } else if (this.config.child_id) {
        const filteredChild = children.find(c => c.id === this.config.child_id);
        currentStars = filteredChild ? (filteredChild.points || 0) : 0;
      } else if (relevantChildren.length > 0) {
        currentStars = relevantChildren[0].points || 0;
      }
    }

    const percentage = displayCost > 0 ? Math.min((currentStars / displayCost) * 100, 100) : 0;

    // Pending claim check
    const pcAttrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config?.entity)) || this.hass?.states?.[this.config?.entity]?.attributes || {};
    const pendingClaims = pcAttrs.pending_reward_claims || [];
    const hasPendingClaim = pendingClaims.some(c =>
      c.reward_id === reward.id && (!childId || c.child_id === childId)
    );

    // Pool mode affordability / redeem state
    const spendable = relevantChild
      ? (typeof relevantChild.spendable_balance === 'number'
          ? relevantChild.spendable_balance
          : (relevantChild.points || 0) - (relevantChild.committed_points || 0))
      : 0;
    const poolFull = enablePoolMode && (
      isJackpot ? jackpotPoolTotal >= displayCost : currentStars >= displayCost
    );

    // Wallet mode: affordability based on gross − committed
    const committedPoints = relevantChild?.committed_points || 0;
    const availablePoints = (relevantChild?.points || 0) - committedPoints;
    const canAfford = relevantChild && availablePoints >= displayCost;
    const isLoading = this._loading[reward.id];
    const isAllocLoading = this._loading[`${reward.id}_alloc`];

    // Availability (quantity / expiration)
    const isSoldOut = reward.is_sold_out === true;
    const isExpired = reward.is_expired === true;
    const isUnavailable = isSoldOut || isExpired;
    const daysUntilExpiry = (typeof reward.days_until_expiry === 'number')
      ? reward.days_until_expiry
      : null;
    const availabilityBadge = this._renderAvailabilityBadge(reward, isSoldOut, isExpired, daysUntilExpiry);

    // Pool-mode controls render below the progress bar (full width) rather than
    // in the right column, so the progress bar keeps its full width.
    const showPoolControls = enablePoolMode && childId && !hasPendingClaim && !isUnavailable;

    return html`
      <div class="reward-row ${isJackpot ? 'jackpot' : ''} ${hasPendingClaim ? 'pending-approval' : ''} ${enablePoolMode ? 'pool-mode' : ''} ${isUnavailable ? 'unavailable' : ''}">
        <div class="cost-badge">
          <ha-icon icon="${pointsIcon}"></ha-icon>
          <span class="cost-value">${displayCost}</span>
          <span class="cost-label">${pointsName}</span>
        </div>
        <div class="reward-details">
          ${isJackpot ? html`<div class="jackpot-label"><span>&#127920;</span> ${this._t('rewards.jackpot')}</div>` : ''}
          <div class="reward-name">${reward.name}</div>
          ${availabilityBadge}
          ${hasDescription
            ? html`<div class="reward-description">${reward.description}</div>`
            : ""}

          ${isJackpot
            ? this._renderJackpotProgress(reward, childContributions, currentStars, pointsIcon, displayCost)
            : this._renderRegularProgress(currentStars, displayCost, percentage, pointsIcon)}

          ${showPoolControls
            ? this._renderPoolControls(reward, relevantChild, spendable, poolFull, isAllocLoading, isLoading)
            : ''}

          ${hasPendingClaim ? html`
            <div class="pending-label">
              <ha-icon icon="mdi:clock-outline"></ha-icon>
              ${this._t('rewards.awaiting_approval')}
            </div>
          ` : ''}

          ${showChildBadges && !isJackpot
            ? html`
                <div class="assigned-children">
                  ${assignedTo.length === 0
                    ? html`<span class="child-badge all-children">${this._t('rewards.all_children')}</span>`
                    : assignedTo.map(
                        (cid) =>
                          html`<span class="child-badge">${childMap[cid] || cid}</span>`
                      )}
                </div>
              `
            : ""}
        </div>
        <div class="reward-right-col">
          ${!enablePoolMode && !hasPendingClaim && childId && !isUnavailable ? html`
            <button
              class="claim-btn ${!canAfford ? 'cant-afford' : ''}"
              ?disabled="${!canAfford || isLoading}"
              @click="${(e) => { e.stopPropagation(); this._promptClaim(reward, relevantChild); }}"
              title="${canAfford ? this._t('rewards.claim_reward') : this._t('rewards.not_enough_points')}"
            >
              <ha-icon icon="${isLoading ? 'mdi:loading' : rewardIcon}"></ha-icon>
            </button>
          ` : html`
            <div class="reward-icon-container" title="${isUnavailable ? this._t('rewards.unavailable_hint') : ''}">
              <ha-icon icon="${rewardIcon}"></ha-icon>
            </div>
          `}
        </div>
      </div>
    `;
  }

  _renderAvailabilityBadge(reward, isSoldOut, isExpired, daysUntilExpiry) {
    if (isExpired) {
      return html`<div class="availability-badge badge-expired">${this._t('rewards.expired')}</div>`;
    }
    if (isSoldOut) {
      return html`<div class="availability-badge badge-sold-out">${this._t('rewards.sold_out')}</div>`;
    }
    const badges = [];
    if (typeof reward.quantity === 'number' && reward.quantity > 0 && reward.quantity <= 3) {
      badges.push(html`<div class="availability-badge badge-low-stock">${this._t('rewards.only_n_left', { count: reward.quantity })}</div>`);
    }
    if (typeof daysUntilExpiry === 'number' && daysUntilExpiry >= 0 && daysUntilExpiry <= 7) {
      const label = daysUntilExpiry === 0
        ? this._t('rewards.expires_today')
        : (daysUntilExpiry === 1
            ? this._t('rewards.expires_in_days', { count: daysUntilExpiry })
            : this._t('rewards.expires_in_days_plural', { count: daysUntilExpiry }));
      badges.push(html`<div class="availability-badge badge-expiring-soon">${label}</div>`);
    }
    if (badges.length === 0) return '';
    return html`<div class="availability-badges">${badges}</div>`;
  }

  _renderPoolControls(reward, child, spendable, poolFull, isAllocLoading, isRedeemLoading) {
    if (poolFull) {
      return html`
        <div class="pool-controls pool-controls-redeem">
          <button
            class="redeem-btn"
            ?disabled="${isRedeemLoading}"
            @click="${(e) => { e.stopPropagation(); this._handleRedeem(reward, child); }}"
            title="${this._t('rewards.redeem')}"
          >
            <ha-icon icon="${isRedeemLoading ? 'mdi:loading' : 'mdi:check-circle'}"></ha-icon>
            ${this._t('rewards.redeem')}
          </button>
        </div>
      `;
    }
    const amounts = this._quickDepositAmounts();
    return html`
      <div class="pool-controls">
        ${amounts.map((amt) => {
          const disabled = !child || spendable < amt || isAllocLoading;
          const tooltip = this._depositTooltip({ child, spendable, amt, isAllocLoading });
          return html`
            <button
              class="alloc-btn"
              ?disabled="${disabled}"
              @click="${(e) => { e.stopPropagation(); this._handleAllocate(reward, child, amt); }}"
              title="${tooltip}"
              aria-label="${tooltip}"
            >+${amt}</button>
          `;
        })}
        ${this._renderCustomDeposit(reward, child, spendable, isAllocLoading)}
      </div>
    `;
  }

  _depositTooltip({ child, spendable, amt, isAllocLoading }) {
    if (isAllocLoading) return this._t('rewards.deposit_disabled_loading');
    if (!child) return this._t('rewards.deposit_disabled_no_child');
    if (spendable < amt) {
      return this._t('rewards.deposit_disabled_insufficient', { spendable });
    }
    return this._t('rewards.deposit_points', { amount: amt });
  }

  /**
   * Quick-deposit button amounts from config.deposit_amounts (#559). Sanitised
   * to positive integers (deduped, order preserved); falls back to [1, 5, 10]
   * when unset or invalid so existing cards are unaffected.
   */
  _quickDepositAmounts() {
    const raw = this.config && this.config.deposit_amounts;
    if (Array.isArray(raw)) {
      const cleaned = [];
      for (const v of raw) {
        const n = Math.floor(Number(v));
        if (Number.isFinite(n) && n >= 1 && !cleaned.includes(n)) cleaned.push(n);
      }
      if (cleaned.length) return cleaned;
    }
    return [1, 5, 10];
  }

  /**
   * Deposit a typed custom amount (#559). The <input> is uncontrolled — we read
   * it from the DOM on submit so keystrokes don't trigger re-renders (which
   * would steal focus). The backend silently caps the amount to the child's
   * spendable balance and the pool's remaining room.
   */
  _handleCustomDeposit(reward, child, e) {
    const wrap = e.target.closest(".deposit-custom");
    const input = wrap && wrap.querySelector("input");
    if (!input) return;
    const amount = Math.floor(Number(input.value));
    if (!Number.isFinite(amount) || amount < 1) return;
    input.value = "";
    this._handleAllocate(reward, child, amount);
  }

  /**
   * The "type any amount + Add" control shown beside the quick-deposit buttons.
   * Shared by the classic and designed layouts; `cls` carries layout-specific
   * button/input classes so each matches its surrounding style.
   */
  _renderCustomDeposit(reward, child, spendable, isAllocLoading, cls = {}) {
    if (!child) return "";
    const disabled = spendable < 1 || isAllocLoading;
    const inputCls = cls.input || "";
    const btnCls = cls.button || "alloc-btn";
    const submit = (e) => { e.stopPropagation(); this._handleCustomDeposit(reward, child, e); };
    return html`
      <span class="deposit-custom">
        <input
          type="number" min="1" inputmode="numeric"
          class="${inputCls}"
          placeholder="${this._t('rewards.deposit_custom_placeholder')}"
          aria-label="${this._t('rewards.deposit_custom_label')}"
          ?disabled="${disabled}"
          @click="${(e) => e.stopPropagation()}"
          @keydown="${(e) => { if (e.key === 'Enter') { e.preventDefault(); submit(e); } }}"
        >
        <button
          class="${btnCls} deposit-add"
          ?disabled="${disabled}"
          @click="${submit}"
          title="${this._t('rewards.deposit_custom_add')}"
        >${this._t('rewards.deposit_custom_add')}</button>
      </span>
    `;
  }

  _promptClaim(reward, child) {
    if (!child || !reward) return;
    this._pendingClaim = { ...reward, _child: child };
  }

  _cancelClaim() {
    this._pendingClaim = null;
  }

  async _confirmClaim() {
    const pending = this._pendingClaim;
    if (!pending) return;
    const child = pending._child;
    this._pendingClaim = null;
    return this._handleClaim(pending, child);
  }

  async _handleClaim(reward, child) {
    if (!child || !reward) return;
    if (this._loading[reward.id]) return;
    this._loading = { ...this._loading, [reward.id]: true };
    this.requestUpdate();
    try {
      // Prefer pressing the per-child/per-reward button entity so HA
      // state-trigger automations on button.taskmate_<child>_claim_<reward>
      // fire. Falls back to the service call if the entity isn't registered.
      const buttonEntityId = window.__taskmate_find_button
        && window.__taskmate_find_button(this.hass, child.id, "claim", reward.id);
      if (buttonEntityId) {
        await this.hass.callService("button", "press", { entity_id: buttonEntityId });
      } else {
        await this.hass.callService("taskmate", "claim_reward", {
          reward_id: reward.id,
          child_id: child.id,
        });
      }
    } catch (e) {
      console.error("Failed to claim reward:", e);
      if (this.hass && this.hass.callService) {
        this.hass.callService("persistent_notification", "create", {
          title: this._t('approvals.error_title'),
          message: this._t('approvals.error_failed_service', {
            service: "claim reward",
            message: e && e.message ? e.message : String(e),
          }),
          notification_id: `taskmate_reward_claim_${reward.id}`,
        });
      }
    } finally {
      this._loading = { ...this._loading, [reward.id]: false };
      this.requestUpdate();
    }
  }

  async _handleAllocate(reward, child, points) {
    if (!child || !reward) return;
    const key = `${reward.id}_alloc`;
    if (this._loading[key]) return;
    this._loading = { ...this._loading, [key]: true };
    this.requestUpdate();
    try {
      await this.hass.callService("taskmate", "allocate_points_to_pool", {
        child_id: child.id,
        reward_id: reward.id,
        points,
      });
    } catch (e) {
      console.error("Failed to allocate points to pool:", e);
      if (this.hass && this.hass.callService) {
        this.hass.callService("persistent_notification", "create", {
          title: this._t('approvals.error_title'),
          message: this._t('approvals.error_failed_service', {
            service: "allocate_points_to_pool",
            message: e && e.message ? e.message : String(e),
          }),
          notification_id: `taskmate_pool_alloc_${reward.id}`,
        });
      }
    } finally {
      this._loading = { ...this._loading, [key]: false };
      this.requestUpdate();
    }
  }

  async _handleRedeem(reward, child) {
    return this._promptClaim(reward, child);
  }

  _renderRegularProgress(currentStars, cost, percentage, pointsIcon) {
    return html`
      <div class="progress-section">
        <div class="progress-bar-container">
          <div class="progress-bar">
            <div class="progress-fill" style="width: ${percentage}%"></div>
          </div>
          <span class="progress-text">${currentStars}/${cost} <ha-icon icon="${pointsIcon}" style="--mdc-icon-size: 14px;"></ha-icon></span>
        </div>
      </div>
    `;
  }

  /**
   * Get the display cost for a reward based on child context.
   * For non-jackpot rewards, each child has their own calculated cost.
   * For jackpot rewards, all children share the same cost.
   */
  _getDisplayCost(reward) {
    // All reward costs are static — just return reward.cost
    // calculated_costs is still provided by the sensor for jackpot display purposes
    const calculatedCosts = reward.calculated_costs || {};

    if (this.config.child_id && calculatedCosts[this.config.child_id]) {
      return calculatedCosts[this.config.child_id];
    }

    return reward.cost;
  }

  _renderJackpotProgress(reward, childContributions, totalStars, pointsIcon, displayCost) {
    const cost = displayCost || reward.cost;

    // For weighted display: each child's segment shows their progress toward their expected share
    // The meter shows how much of their "responsibility" each child has fulfilled
    const hasWeightedData = childContributions.some(c => c.expectedContribution > 0);

    // Calculate weighted segments - each segment width = (child's share of goal) * (their progress %)
    // This way, if a child has 50% share and is at 100% progress, they fill 50% of the bar
    const segments = childContributions.map((contrib) => {
      let width;
      if (hasWeightedData && contrib.shareOfGoal > 0) {
        // Weighted: segment width = share of goal * progress percentage
        // e.g., 40% share at 50% progress = 20% of bar filled
        width = (contrib.shareOfGoal / 100) * (contrib.weightedProgress / 100) * 100;
      } else {
        // Fallback: raw contribution relative to cost
        width = cost > 0 ? Math.min((contrib.points / cost) * 100, 100) : 0;
      }
      return {
        ...contrib,
        width: width
      };
    });

    return html`
      <div class="progress-section">
        <div class="progress-bar-container">
          <div class="jackpot-progress-bar">
            ${segments.map((seg) => html`
              <div class="jackpot-segment color-${seg.colorIndex}"
                   style="width: ${seg.width}%"></div>
            `)}
          </div>
          <span class="progress-text">${totalStars}/${cost} <ha-icon icon="${pointsIcon}" style="--mdc-icon-size: 14px;"></ha-icon></span>
        </div>
        <div class="jackpot-breakdown">
          ${childContributions.map((contrib) => html`
            <span class="jackpot-child-contribution">
              <span class="color-dot color-${contrib.colorIndex}"></span>
              <strong>${contrib.name}</strong>:
              ${contrib.points}
              <ha-icon icon="${pointsIcon}" style="--mdc-icon-size: 12px;"></ha-icon>
              ${hasWeightedData && contrib.expectedContribution > 0 ? html`
                <span class="jackpot-pct">(${Math.round(contrib.weightedProgress)}%)</span>
              ` : ''}
            </span>
          `)}
        </div>
      </div>
    `;
  }

  /* ══════════════════════════════════════════════════════════════════════
     DESIGNED STYLES (playroom / console / cleanpro) — see taskmate-design.js
     Ported from docs/design/redesigns (§card-rewards). FIXED COSTS ONLY —
     never any dynamic/surge pricing. All existing claim/pool/confirm logic is
     preserved: designed buttons call the SAME handlers as classic.
  ══════════════════════════════════════════════════════════════════════ */

  _designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }

  _av(child, tone, size) {
    const a = (child && child.avatar) || "";
    const inner = a.startsWith("mdi:")
      ? html`<ha-icon icon="${a}"></ha-icon>`
      : a
        ? html`<img src="${a}" alt="${child.name}">`
        : ((child && child.name) || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
    return html`<div class="av" style="--av:${size}px;--ac:${tone}">${inner}</div>`;
  }

  /**
   * Compute all per-reward display data once, mirroring the classic
   * _renderRewardRow calculations. FIXED COST is reward.cost (via
   * _getDisplayCost — static only). Returns everything the designed rows need.
   */
  _rewardData(reward, children) {
    const assignedTo = reward.assigned_to || [];
    const isJackpot = reward.is_jackpot || false;
    const cardForcesPool = this.config.enable_pool_mode === true;
    // Jackpot rewards are always pool-mode (#552): everyone deposits into the
    // shared jar and it's redeemed when full. Combined points only work via the
    // pool, so jackpots never show the single-child claim button.
    const enablePoolMode = cardForcesPool || reward.pool_enabled === true || isJackpot;
    const displayCost = this._getDisplayCost(reward);

    const relevantChildren = assignedTo.length === 0
      ? children
      : children.filter(c => assignedTo.includes(c.id));

    const childId = this.config?.child_id || this._selectedChildId;
    const relevantChild = childId
      ? children.find(c => c.id === childId)
      : relevantChildren[0];

    const poolAllocations = reward.pool_allocations || {};
    const jackpotPoolTotal = typeof reward.jackpot_pool_total === 'number' ? reward.jackpot_pool_total : 0;

    let currentStars = 0;
    const childContributions = [];

    if (isJackpot) {
      relevantChildren.forEach((child, index) => {
        const points = enablePoolMode
          ? (poolAllocations[child.id] || 0)
          : (child.points || 0);
        currentStars += points;
        childContributions.push({
          name: child.name,
          child,
          points,
          tone: this._designTone(index),
        });
      });
    } else if (enablePoolMode) {
      currentStars = childId ? (poolAllocations[childId] || 0) : 0;
    } else if (this.config.child_id) {
      const fc = children.find(c => c.id === this.config.child_id);
      currentStars = fc ? (fc.points || 0) : 0;
    } else if (relevantChildren.length > 0) {
      currentStars = relevantChildren[0].points || 0;
    }

    const percentage = displayCost > 0 ? Math.min((currentStars / displayCost) * 100, 100) : 0;

    const pcAttrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config?.entity))
      || this.hass?.states?.[this.config?.entity]?.attributes || {};
    const pendingClaims = pcAttrs.pending_reward_claims || [];
    const hasPendingClaim = pendingClaims.some(c =>
      c.reward_id === reward.id && (!childId || c.child_id === childId)
    );

    const spendable = relevantChild
      ? (typeof relevantChild.spendable_balance === 'number'
          ? relevantChild.spendable_balance
          : (relevantChild.points || 0) - (relevantChild.committed_points || 0))
      : 0;
    const poolFull = enablePoolMode && (
      isJackpot ? jackpotPoolTotal >= displayCost : currentStars >= displayCost
    );

    const committedPoints = relevantChild?.committed_points || 0;
    const availablePoints = (relevantChild?.points || 0) - committedPoints;
    const canAfford = !!relevantChild && availablePoints >= displayCost;

    const isSoldOut = reward.is_sold_out === true;
    const isExpired = reward.is_expired === true;
    const isUnavailable = isSoldOut || isExpired;
    const daysUntilExpiry = (typeof reward.days_until_expiry === 'number')
      ? reward.days_until_expiry : null;

    return {
      reward, isJackpot, enablePoolMode, displayCost, relevantChildren, relevantChild,
      childId, currentStars, childContributions, percentage, hasPendingClaim,
      spendable, poolFull, canAfford, isSoldOut, isExpired, isUnavailable, daysUntilExpiry,
      showClaim: !enablePoolMode && !hasPendingClaim && childId && !isUnavailable,
      showPoolControls: enablePoolMode && childId && !hasPendingClaim && !isUnavailable,
    };
  }

  /** Designed availability chip label/tone for a reward, or null. */
  _designAvail(d) {
    if (d.isExpired) return { label: this._t('rewards.expired'), tone: 'muted' };
    if (d.isSoldOut) return { label: this._t('rewards.sold_out'), tone: 'muted' };
    if (typeof d.reward.quantity === 'number' && d.reward.quantity > 0 && d.reward.quantity <= 3) {
      return { label: this._t('rewards.only_n_left', { count: d.reward.quantity }), tone: 'bad' };
    }
    if (typeof d.daysUntilExpiry === 'number' && d.daysUntilExpiry >= 0 && d.daysUntilExpiry <= 7) {
      const label = d.daysUntilExpiry === 0
        ? this._t('rewards.expires_today')
        : (d.daysUntilExpiry === 1
            ? this._t('rewards.expires_in_days', { count: d.daysUntilExpiry })
            : this._t('rewards.expires_in_days_plural', { count: d.daysUntilExpiry }));
      return { label, tone: 'warn' };
    }
    return null;
  }

  _renderDesigned(design) {
    const entity = this.hass.states[this.config.entity];
    const hd = _safeColor(this.config.header_color, '#e67e22');

    if (!entity) {
      return html`<ha-card class="tmd" style="--hd:${hd}">
        ${this._designHeader(0)}
        <div class="tmd-bd"><div class="tmd-empty">${this._t('common.entity_not_found', { entity: this.config.entity })}</div></div>
      </ha-card>`;
    }

    const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {};
    const allRewards = attrs.rewards || [];
    const children = attrs.children || [];
    const pointsIcon = attrs.points_icon || "mdi:star";
    const pointsName = attrs.points_name || this._t("common.stars");

    let rewards = allRewards;
    if (this.config.child_id) {
      rewards = allRewards.filter((reward) => {
        const assignedTo = reward.assigned_to || [];
        return assignedTo.length === 0 || assignedTo.includes(this.config.child_id);
      });
    }

    const sortedRewards = [...rewards].sort((a, b) => {
      if (a.is_jackpot && !b.is_jackpot) return -1;
      if (!a.is_jackpot && b.is_jackpot) return 1;
      return this._getDisplayCost(a) - this._getDisplayCost(b);
    });

    const cardForcesPool = this.config.enable_pool_mode === true;
    const activeChildId = this.config.child_id || this._selectedChildId;
    const activeChild = activeChildId ? children.find((c) => c.id === activeChildId) : null;
    const anyPoolReward = sortedRewards.some((r) => cardForcesPool || r.pool_enabled === true || r.is_jackpot);

    const header = this._designHeader(rewards.length);

    const body = sortedRewards.length === 0
      ? html`<div class="tmd-empty">${this._t('rewards.empty_title')}<br>${this._t('rewards.empty_subtitle')}</div>`
      : html`
        ${this._designChildTabs(children)}
        ${(anyPoolReward && activeChild) || activeChild
          ? this._designChildSelector(activeChild, pointsIcon, pointsName, design)
          : ''}
        <div class="rw-list ${this.config.expand_to_fit === false ? 'rw-scroll' : ''}">
          ${sortedRewards.map((r) => this._designRewardRow(r, children, pointsIcon, pointsName, design))}
        </div>`;

    return html`
      <ha-card class="tmd" style="--hd:${hd}">
        ${header}
        <div class="tmd-bd">${body}</div>
      </ha-card>
      ${this._renderConfirmDialog(pointsIcon, pointsName)}
    `;
  }

  _designHeader(count) {
    return html`
      <div class="tmd-hd">
        <span class="ic">🎁</span>
        <span class="tt">${this.config.title || this._t('rewards.default_title')}${count
          ? html`<small>${count === 1
              ? this._t('rewards.reward_count_singular', { count })
              : this._t('rewards.reward_count_plural', { count })}</small>` : ''}</span>
      </div>`;
  }

  _designChildSelector(child, _pointsIcon, _pointsName, _design) {
    const spendable = typeof child.spendable_balance === 'number'
      ? child.spendable_balance : (child.points || 0);
    return html`
      <div class="row rw-sel" style="--ac:${this._designTone(0)}">
        ${this._av(child, this._designTone(0), 38)}
        <div class="rw-sel-name">${child.name}</div>
        <span class="chip soft">
          <span style="color:var(--tmd-gold)">⭐</span>
          ${this._t('rewards.spendable_balance')}: ${spendable}
        </span>
      </div>`;
  }

  _designRewardRow(reward, children, pointsIcon, pointsName, _design) {
    const d = this._rewardData(reward, children);
    const tone = this._designTone((reward.id || reward.name || '').length);
    const isLoading = this._loading[reward.id];
    const avail = this._designAvail(d);
    const emoji = "🎁";

    // Child-assignment badges — honour show_child_badges (default on), classic-parity:
    // only for non-jackpot rewards; "all children" chip when unassigned.
    const showChildBadges = this.config.show_child_badges !== false;
    const assignedTo = reward.assigned_to || [];
    const childMap = {};
    children.forEach((c) => { childMap[c.id] = c.name; });
    const badges = showChildBadges && !d.isJackpot
      ? (assignedTo.length === 0
          ? html`<span class="chip soft rw-badge rw-badge-all">${this._t('rewards.all_children')}</span>`
          : assignedTo.map((cid) => html`<span class="chip rw-badge">${childMap[cid] || cid}</span>`))
      : '';

    const costBadge = html`<span class="chip rw-cost">⭐ ${d.displayCost} ${pointsName}</span>`;

    const bar = d.isJackpot
      ? html`<div class="bar rw-pool-bar">
          ${d.childContributions.map((c) => {
            const w = d.displayCost > 0 ? Math.min((c.points / d.displayCost) * 100, 100) : 0;
            return html`<i style="width:${w}%;background:${c.tone}"></i>`;
          })}
        </div>`
      : html`<div class="bar"><i style="width:${d.percentage}%;${d.canAfford ? 'background:var(--tmd-good)' : ''}"></i></div>`;

    const claimBtn = d.showClaim
      ? html`<button class="btn sm ${d.canAfford ? 'good' : ''}"
          ?disabled="${!d.canAfford || isLoading}"
          @click="${(e) => { e.stopPropagation(); this._promptClaim(reward, d.relevantChild); }}"
          title="${d.canAfford ? this._t('rewards.claim_reward') : this._t('rewards.not_enough_points')}"
        >${this._t('rewards.claim_reward')}</button>`
      : '';

    const poolControls = d.showPoolControls
      ? (d.poolFull
        ? html`<button class="btn sm good"
            ?disabled="${isLoading}"
            @click="${(e) => { e.stopPropagation(); this._handleRedeem(reward, d.relevantChild); }}"
          >${this._t('rewards.redeem')}</button>`
        : html`<span class="row rw-pool-btns">
            ${this._quickDepositAmounts().map((amt, i, arr) => {
              const disabled = !d.relevantChild || d.spendable < amt || this._loading[`${reward.id}_alloc`];
              return html`<button class="btn sm ${i === arr.length - 1 ? '' : 'ghost'}"
                ?disabled="${disabled}"
                title="${this._depositTooltip({ child: d.relevantChild, spendable: d.spendable, amt, isAllocLoading: this._loading[`${reward.id}_alloc`] })}"
                @click="${(e) => { e.stopPropagation(); this._handleAllocate(reward, d.relevantChild, amt); }}"
              >+${amt}</button>`;
            })}
            ${this._renderCustomDeposit(reward, d.relevantChild, d.spendable, this._loading[`${reward.id}_alloc`], { button: 'btn sm', input: 'rw-deposit-input' })}
          </span>`)
      : '';

    const statusLine = d.hasPendingClaim
      ? html`<span class="muted rw-status">⏳ ${this._t('rewards.awaiting_approval')}</span>`
      : d.canAfford && d.showClaim
        ? html`<span class="rw-status rw-status-good">${this._t('reward_progress.ready_to_claim')}</span>`
        : html`<span class="muted rw-status">${d.currentStars} / ${d.displayCost}</span>`;

    return html`
      <div class="rw-card ${d.isJackpot ? 'rw-jackpot' : ''} ${d.isUnavailable ? 'rw-unavail' : ''}"
           style="--ac:${tone}">
        <div class="row rw-top">
          <span class="rw-emoji">${emoji}</span>
          <div class="rw-info">
            ${d.isJackpot ? html`<span class="chip rw-jackpot-label">🎰 ${this._t('rewards.jackpot')}</span>` : ''}
            <div class="rw-name">${reward.name}</div>
            ${reward.description ? html`<div class="muted rw-desc">${reward.description}</div>` : ''}
            ${avail ? html`<span class="chip soft rw-avail rw-avail-${avail.tone}">${avail.label}</span>` : ''}
            ${badges ? html`<div class="rw-badges">${badges}</div>` : ''}
          </div>
          ${costBadge}
        </div>
        ${bar}
        ${d.isJackpot ? html`
          <div class="row rw-pool-foot">
            ${d.childContributions.map((c) => html`
              <span class="chip rw-contrib">${this._av(c.child, c.tone, 16)} ${c.points}</span>`)}
          </div>` : ''}
        <div class="row rw-foot">
          ${statusLine}
          <span style="flex:1"></span>
          ${claimBtn}${poolControls}
        </div>
      </div>`;
  }

  /** The confirm dialog markup, shared by classic + designed paths. */
  _renderConfirmDialog(pointsIcon, pointsName) {
    if (!this._pendingClaim) return '';
    return html`
      <div class="confirm-overlay" @click="${this._cancelClaim}">
        <div class="confirm-dialog" @click="${(e) => e.stopPropagation()}">
          <ha-icon class="dialog-icon" icon="mdi:gift-outline"></ha-icon>
          <div class="dialog-title">${this._t('rewards.confirm_title')}</div>
          <div class="dialog-body">
            ${(() => {
              const r = this._pendingClaim;
              const child = r._child;
              const currentPoints = child ? (typeof child.spendable_balance === 'number' ? child.spendable_balance : (child.points || 0)) : null;
              const cost = r.cost;
              const remaining = currentPoints !== null ? currentPoints - cost : null;
              return html`
                <div style="margin-bottom: 12px;">${this._t('rewards.confirm_body', { name: r.name })}</div>
                ${currentPoints !== null ? html`
                  <div class="dialog-points-summary">
                    <div class="dialog-points-row">
                      <span>${this._t('rewards.confirm_your_points', { name: pointsName })}</span>
                      <span class="points-val">
                        <ha-icon icon="${pointsIcon}" style="--mdc-icon-size:14px;"></ha-icon>
                        ${currentPoints}
                      </span>
                    </div>
                    <div class="dialog-points-row cost">
                      <span>${this._t('rewards.confirm_cost')}</span>
                      <span class="points-val">
                        <ha-icon icon="${pointsIcon}" style="--mdc-icon-size:14px;"></ha-icon>
                        −${cost}
                      </span>
                    </div>
                    <div class="dialog-points-row remaining">
                      <span>${this._t('rewards.confirm_remaining')}</span>
                      <span class="points-val">
                        <ha-icon icon="${pointsIcon}" style="--mdc-icon-size:14px;"></ha-icon>
                        ${remaining}
                      </span>
                    </div>
                  </div>
                ` : ''}
              `;
            })()}
          </div>
          <div class="dialog-buttons">
            <button class="btn-cancel" @click="${this._cancelClaim}">${this._t('common.cancel')}</button>
            <button class="btn-confirm" @click="${this._confirmClaim}">${this._t('rewards.confirm_claim_btn')}</button>
          </div>
        </div>
      </div>`;
  }
}

// Card Editor for visual configuration in Lovelace UI
class TaskMateRewardsCardEditor extends LitElement {
  static get properties() {
    return {
      hass: { type: Object },
      config: { type: Object },
    };
  }

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

  static get styles() {
    return css`
      :host { display: block; }
      ha-form { display: block; margin-bottom: 16px; }

      .colour-field {
        display: flex;
        flex-direction: column;
        gap: 8px;
        padding: 12px 16px;
        border: 1px solid var(--outline-color, var(--divider-color, #e0e0e0));
        border-radius: 4px;
        background: var(--mdc-text-field-fill-color, var(--card-background-color));
      }
      .colour-field-label {
        font-size: 0.82rem;
        color: var(--primary-color);
        font-weight: 500;
      }
      .colour-field-body {
        display: flex;
        align-items: center;
        gap: 12px;
        flex-wrap: wrap;
      }
      .colour-swatch-wrapper {
        position: relative;
        width: 36px;
        height: 36px;
        border-radius: 50%;
        overflow: hidden;
        cursor: pointer;
        border: 2px solid var(--divider-color, #e0e0e0);
        flex-shrink: 0;
      }
      .colour-swatch-wrapper input[type="color"] {
        position: absolute;
        inset: 0;
        opacity: 0;
        cursor: pointer;
        border: 0;
        padding: 0;
      }
      .colour-swatch-preview { position: absolute; inset: 0; pointer-events: none; }
      .colour-hex {
        font-family: var(--code-font-family, monospace);
        font-size: 0.85rem;
        color: var(--secondary-text-color);
        min-width: 70px;
      }
      .colour-presets { display: flex; gap: 6px; flex-wrap: wrap; }
      .preset-swatch {
        width: 22px;
        height: 22px;
        border-radius: 50%;
        cursor: pointer;
        border: 2px solid var(--divider-color, #e0e0e0);
        transition: transform 0.1s;
        padding: 0;
      }
      .preset-swatch:hover { transform: scale(1.15); }
      .preset-swatch.active {
        border-color: var(--primary-text-color);
        box-shadow: 0 0 0 2px var(--primary-color);
      }
      .colour-reset {
        font-size: 0.78rem;
        color: var(--secondary-text-color);
        background: none;
        border: 1px solid var(--divider-color, #e0e0e0);
        border-radius: 4px;
        padding: 4px 10px;
        cursor: pointer;
        margin-left: auto;
      }
      .colour-helper {
        color: var(--secondary-text-color);
        font-size: 0.82rem;
        line-height: 1.3;
      }
    `;
  }

  setConfig(config) {
    this.config = config;
  }

  // Build the ha-form schema based on the current state (e.g. child list from the entity).
  _buildSchema() {
    const entity = this.config?.entity ? this.hass?.states?.[this.config.entity] : null;
    const children = entity?.attributes?.children || [];

    const childOptions = [
      { value: '__all__', label: this._t('rewards.editor.filter_by_child_all') },
      ...children.map((c) => ({ value: c.id, label: c.name })),
    ];

    return [
      {
        name: 'entity',
        selector: { entity: { domain: 'sensor' } },
      },
      {
        name: 'title',
        selector: { text: {} },
      },
      {
        name: 'child_id',
        selector: { select: { options: childOptions, mode: 'dropdown' } },
      },
      {
        name: 'card_design',
        selector: {
          select: {
            options: window.__taskmate_design
              ? window.__taskmate_design.editorOptions(this._t.bind(this))
              : [{ value: 'global', label: 'Use global default' }],
            mode: 'dropdown',
          },
        },
      },
      {
        name: 'show_child_badges',
        selector: { boolean: {} },
      },
      {
        name: 'enable_pool_mode',
        selector: { boolean: {} },
      },
      {
        name: 'expand_to_fit',
        selector: { boolean: {} },
      },
    ];
  }

  _computeLabel = (schemaEntry) => {
    const labels = {
      entity: this._t('rewards.editor.entity'),
      title: this._t('rewards.editor.title'),
      child_id: this._t('rewards.editor.filter_by_child'),
      card_design: this._t('common.design.field_label'),
      show_child_badges: this._t('rewards.editor.show_child_badges'),
      enable_pool_mode: this._t('rewards.editor.enable_pool_mode'),
      expand_to_fit: this._t('rewards.editor.expand_to_fit'),
    };
    return labels[schemaEntry.name] ?? schemaEntry.name;
  };

  _computeHelper = (schemaEntry) => {
    const helpers = {
      entity: this._t('rewards.editor.entity_helper'),
      title: this._t('rewards.editor.title_helper'),
      child_id: this._t('rewards.editor.filter_by_child_helper'),
      show_child_badges: this._t('rewards.editor.show_child_badges_helper'),
      enable_pool_mode: this._t('rewards.editor.enable_pool_mode_helper'),
      expand_to_fit: this._t('rewards.editor.expand_to_fit_helper'),
    };
    return helpers[schemaEntry.name] ?? '';
  };

  render() {
    if (!this.hass || !this.config) {
      return html``;
    }
    // ha-form treats missing keys as empty; normalise for consistent values.
    const data = {
      entity: this.config.entity || '',
      title: this.config.title || '',
      child_id: this.config.child_id || '__all__',
      card_design: this.config.card_design || 'global',
      show_child_badges: this.config.show_child_badges !== false,
      enable_pool_mode: this.config.enable_pool_mode === true,
      expand_to_fit: this.config.expand_to_fit !== false,
    };

    return html`
      <ha-form
        .hass=${this.hass}
        .data=${data}
        .schema=${this._buildSchema()}
        .computeLabel=${this._computeLabel}
        .computeHelper=${this._computeHelper}
        @value-changed=${this._formChanged}
      ></ha-form>
      ${this._renderColourPicker('header_color', '#e67e22')}
    `;
  }

  _renderColourPicker(key, defaultValue) {
    const d = window.__taskmate_design;
    const current = this.config[key] || defaultValue;
    if (!d || !d.colourPicker) return html``;
    return d.colourPicker({
      defaultValue, current,
      label: this._t('common.editor.header_colour'),
      helper: this._t('common.editor.header_colour_helper'),
      resetLabel: this._t('common.reset'),
      onInput: (v) => this._updateConfig(key, v),
      onPreset: (v) => this._updateConfig(key, v),
      onReset: () => this._updateConfig(key, defaultValue),
    });
  }

  _formChanged(e) {
    const newValues = e.detail.value || {};
    // Merge into config, removing keys that are empty/default so the YAML stays clean.
    const newConfig = { ...this.config };
    for (const [key, value] of Object.entries(newValues)) {
      if (
        value === '' || value === null || value === undefined
        || (key === 'child_id' && value === '__all__')
      ) {
        delete newConfig[key];
      } else if (key === 'card_design' && value === 'global') {
        delete newConfig[key];
      } else if (key === 'show_child_badges' && value === true) {
        // Default is true — omit to keep config minimal
        delete newConfig[key];
      } else if (key === 'enable_pool_mode' && value === false) {
        delete newConfig[key];
      } else if (key === 'expand_to_fit' && value === true) {
        // Default is true (fluid) — omit to keep config minimal
        delete newConfig[key];
      } else {
        newConfig[key] = value;
      }
    }
    this._dispatchConfig(newConfig);
  }

  _updateConfig(key, value) {
    const newConfig = { ...this.config, [key]: value };
    if (value === undefined || value === '' || value === null) {
      delete newConfig[key];
    }
    this._dispatchConfig(newConfig);
  }

  _dispatchConfig(newConfig) {
    this.dispatchEvent(
      new CustomEvent('config-changed', {
        detail: { config: newConfig },
        bubbles: true,
        composed: true,
      })
    );
  }
}

// Register the cards
customElements.define("taskmate-rewards-card", TaskMateRewardsCard);
customElements.define("taskmate-rewards-card-editor", TaskMateRewardsCardEditor);

// Register with Home Assistant
window.customCards = window.customCards || [];
window.customCards.push({
  type: "taskmate-rewards-card",
  name: "TaskMate Rewards Card",
  description: "A card displaying available rewards with star costs",
  preview: true,
  getEntitySuggestion: (hass, entityId) =>
    window.__taskmate_suggest(hass, entityId, "taskmate-rewards-card", "overview"),
});

// Version is injected by the HA resource URL (?v=x.x.x) and read from the DOM
const _tmVersion = new URLSearchParams(
  Array.from(document.querySelectorAll('script[src*="/taskmate-rewards-card.js"]'))
    .map(s => s.src.split("?")[1]).find(Boolean) || ""
).get("v") || "?";
console.info(
  "%c TASKMATE REWARDS CARD %c v" + _tmVersion + " ",
  "background:#e67e22;color:white;font-weight:bold;padding:2px 4px;border-radius:4px 0 0 4px;",
  "background:#2c3e50;color:white;font-weight:bold;padding:2px 4px;border-radius:0 4px 4px 0;"
);
