Ich werde einfach nicht fertig 🙄

Nachdem mir mein Dashboard im hellen Design etwas langweilig wurde, habe ich etwas rumgebastelt. Das Ergebnis wĂĽrde ich euch gerne zeigen.
Gerne auch Verbesserungsvorschläge.

Ist nur ein kleiner Ausschnitt.

Zusätzlich bastele ich noch an einer Bahn-Card.

13 „Gefällt mir“

Schick :+1:

Schick, schick. :ok_hand: Gefällt mir auch sehr gut, bin ja eh mehr für Dark und Blau als Dark Mode finde ich immer ideal. Schlichtes Design und trotzdem viel Informationen auf eine Blick. Die Boxränder gefallen mir auch sehr gut mit dem breiten oberen Rand. Für mich wären es noch zu wenig Seiten (wegen der vielen Geräte), aber das ist immer benutzerspezifisch und kann angepasst werden.

Magst du auch ein wenig Code teilen, dass andere nachbauen können?

2 „Gefällt mir“

Gerne, bin aber im Moment etwas beschäftigt. Werfe demnächst mal Auszüge raus.

1 „Gefällt mir“

Welche Card benutzt du fĂĽr die DB Ansicht?

LG

Könnte diese hier möglicherweise sein:

2 „Gefällt mir“

Hallo Enrico,

die Karte habe ich an mein Design angepasst. Die Grundentitäten kommen aus HACS “db-infoscreen”

hier noch der Kartencode:

type: custom:button-card
entity: sensor.db_infoscreen_muden_mosel_departures
show_name: false
show_icon: false
show_state: false
show_label: true
tap_action:
  action: none
styles:
  card:
    - width: 443px
    - height: 377px
    - margin-top: "-11px"
    - margin-left: "-10.5px"
    - padding: 0
    - background: "linear-gradient(145deg, #1c3a50 0%, #0e2333 40%, #0a1c2b 100%)"
    - border-radius: 16px
    - overflow: hidden
    - font-family: "'Exo 2', sans-serif"
    - color: "#c8dde8"
    - box-shadow: 0 4px 24px rgba(0,0,0,0.5)
    - cursor: default
  grid:
    - grid-template-areas: "\"l\""
    - grid-template-columns: 1fr
    - grid-template-rows: 377px
    - padding: 0
    - margin: 0
    - width: 100%
    - height: 100%
  label:
    - grid-area: l
    - padding: 0
    - margin: 0
    - width: 443px
    - height: 377px
    - display: block
    - position: relative
    - overflow: hidden
  custom_fields:
    topbar:
      - position: absolute
      - top: "0"
      - left: "0"
      - right: "0"
      - bottom: "0"
      - pointer-events: none
      - z-index: "1"
custom_fields:
  topbar: |
    [[[
      return `
        <div style="position:absolute;top:0;left:0;right:0;height:3px;
          background:linear-gradient(90deg,transparent,#4398C3,#7EC8E3,#4398C3,transparent);
          opacity:1;z-index:3;pointer-events:none;"></div>
        <div style="position:absolute;top:-20px;right:-20px;
          width:140px;height:140px;border-radius:50%;
          background:radial-gradient(circle,rgba(67,152,195,0.35) 0%,transparent 70%);
          pointer-events:none;z-index:1;opacity:1;"></div>`;
    ]]]
label: |
  [[[
    return (() => {
      const W = 443, H = 377, PAD = 20;
      const deps = entity.attributes.next_departures || [];
      const now = Date.now();
      const windowMs = 3 * 60 * 60 * 1000;

      const upd = (() => {
        const d = new Date(entity.last_updated);
        return String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0');
      })();

      const LIST_TOP = 80, LIST_BOTTOM = 369;
      const LIST_H = LIST_BOTTOM - LIST_TOP;
      const MARKER_H = 22;

      let html = '';



      html += '<img src="/local/logos/DB_Logo.png" '
        + 'style="position:absolute;top:13px;left:' + PAD + 'px;height:24px;width:auto">';

      html += '<div style="position:absolute;top:16px;left:' + (PAD + 42) + 'px;font-size:16px;'
        + 'font-weight:500;color:#7EC8E3;letter-spacing:0.5px;white-space:nowrap">'
        + 'MÜDEN (MOSEL)&nbsp;<span style="color:#4398C3">→</span>&nbsp;KOBLENZ HBF</div>';

      html += '<div style="position:absolute;top:14px;right:' + PAD + 'px;text-align:right;'
        + 'font-size:10px;color:rgba(200,221,232,1);line-height:1.5">'
        + 'Stand:&nbsp;' + upd + '<br>±2h&nbsp;·&nbsp;IRIS-TTS</div>';

      html += '<div style="position:absolute;top:50px;left:0;width:100%;height:1px;'
        + 'background:rgba(67,152,195,0.2)"></div>';

      html += '<div style="position:absolute;top:60px;left:24px;font-size:12px;'
        + 'text-transform:uppercase;font-weight:500;letter-spacing:0.5px;color:rgba(67,152,195,1)">Zug / Ziel</div>';
      html += '<div style="position:absolute;top:60px;left:280px;font-size:12px;'
        + 'text-transform:uppercase;font-weight:500;letter-spacing:0.5px;color:rgba(67,152,195,1)">Abfahrt</div>';
      html += '<div style="position:absolute;top:60px;right:24px;font-size:12px;'
        + 'text-transform:uppercase;font-weight:500;letter-spacing:0.5px;color:rgba(67,152,195,1)">Gleis</div>';

      const filtered = deps.filter(d => {
        const ts = (d.departure_timestamp || 0) * 1000;
        return ts >= now - windowMs && ts <= now + windowMs;
      });

      const wrap = (inner) => '<div style="position:relative;width:' + W + 'px;height:' + H
        + 'px;overflow:hidden">' + inner + '</div>';

      if (!filtered.length) {
        html += '<div style="position:absolute;top:' + (LIST_TOP + LIST_H/2 - 10) + 'px;left:0;'
          + 'width:100%;text-align:center;font-size:13px;color:rgba(200,221,232,0.4)">'
          + 'Keine Abfahrten im ±2h-Fenster</div>';
        return wrap(html);
      }

      const seq = [];
      let markerPlaced = false;
      for (const d of filtered) {
        const depTs = (d.departure_timestamp || 0) * 1000;
        if (depTs >= now && !markerPlaced) { seq.push({t:'m'}); markerPlaced = true; }
        seq.push({t:'r', d});
      }
      if (!markerPlaced) seq.push({t:'m'});

      const rowCount = filtered.length;
      let rowH = (LIST_H - MARKER_H) / rowCount;
      if (rowH > 74) rowH = 74;
      if (rowH < 44) rowH = 44;

      const totalH = MARKER_H + rowCount * rowH;
      let y = LIST_TOP + Math.max(0, (LIST_H - totalH) / 2);

      let firstFutureStyled = false;

      for (const it of seq) {
        if (it.t === 'm') {
          const my = y + MARKER_H / 2;
          html += '<div style="position:absolute;top:' + (my - 5) + 'px;left:14px;width:172px;height:1px;'
            + 'background:linear-gradient(90deg,#4398C3,transparent)"></div>';
          html += '<div style="position:absolute;top:' + (my - 5) + 'px;left:257px;width:172px;height:1px;'
            + 'background:linear-gradient(270deg,#4398C3,transparent)"></div>';
          html += '<div style="position:absolute;top:' + (my - 12) + 'px;left:0;width:100%;'
            + 'text-align:center;font-size:12px;font-weight:500;letter-spacing:0.5px;color:#4398C3">JETZT</div>';
          y += MARKER_H;
          continue;
        }

        const d = it.d;
        const depTs     = (d.departure_timestamp || 0) * 1000;
        const isPast    = depTs < now;
        const cancelled = !!(d.isCancelled || d.is_cancelled);
        const delayMin  = parseInt(d.delayDeparture || d.delay || 0);
        const isDelayed = delayMin > 0;
        const isEarly   = delayMin < 0;

        const scheduledTime = String(d.scheduledDeparture || '').replace(/[+-]\d+$/,'').trim();
        const actualTime    = String(d.departure_current  || scheduledTime).replace(/[+-]\d+$/,'').trim();
        const train         = String(d.train || d.trainNumber || '?');
        const dest          = String(d.destination || '');
        const plat          = String(d.platform || d.scheduledPlatform || '');

        const opacity = isPast ? '0.38' : '1';
        let bg, border;
        if (cancelled) {
          bg = 'rgba(229,115,115,0.08)'; border = '1px solid rgba(229,115,115,0.35)';
        } else if (!isPast && !firstFutureStyled) {
          bg = 'rgba(67,152,195,0.12)'; border = '1px solid rgba(67,152,195,0.4)';
          firstFutureStyled = true;
        } else {
          bg = isPast ? 'transparent' : 'rgba(255,255,255,0.02)';
          border = '1px solid transparent';
          if (!isPast && !firstFutureStyled) firstFutureStyled = true;
        }

        const badgeColor  = cancelled ? '#e57373' : '#7EC8E3';
        const badgeBorder = cancelled ? 'rgba(229,115,115,0.5)' : 'rgba(67,152,195,0.6)';
        const badgeBg     = cancelled ? 'rgba(229,115,115,0.12)' : 'rgba(67,152,195,0.18)';

        let timeInner;
        if (cancelled) {
          timeInner = '<span style="font-size:14px;font-weight:600;'
            + 'color:rgba(229,115,115,0.5);text-decoration:line-through">' + scheduledTime + '</span>';
        } else if (isDelayed || isEarly) {
          const col  = isDelayed ? '#e57373' : '#81c784';
          const lbl  = isDelayed ? '+' + delayMin + ' min' : delayMin + ' min';
          const bgDl = isDelayed ? 'rgba(229,115,115,0.12)' : 'rgba(129,199,132,0.12)';
          timeInner = '<div style="line-height:1.2">'
            + '<div style="font-size:10px;color:rgba(200,221,232,0.38);text-decoration:line-through">'
            + scheduledTime + '</div>'
            + '<div style="white-space:nowrap">'
            + '<span style="font-size:15px;font-weight:700;color:' + col + '">' + actualTime + '</span>'
            + '<span style="font-size:10px;font-weight:700;color:' + col + ';background:' + bgDl
            + ';border-radius:3px;padding:0 3px;margin-left:3px">' + lbl + '</span>'
            + '</div></div>';
        } else {
          timeInner = '<span style="font-size:15px;font-weight:600;color:#c8dde8">' + scheduledTime + '</span>';
        }

        const pairH = 40;
        const off = Math.max(6, (rowH - pairH) / 2);
        const badgeTop = off;
        const destTop  = off + 24;

        const badges = '<div style="position:absolute;left:10px;top:' + badgeTop + 'px">'
          + '<span style="display:inline-block;background:' + badgeBg + ';border:1px solid ' + badgeBorder
          + ';border-radius:5px;padding:2px 7px;font-size:11px;font-weight:700;color:' + badgeColor
          + ';white-space:nowrap;vertical-align:middle">' + train + '</span>'
          + (cancelled ? '<span style="display:inline-block;margin-left:5px;font-size:10px;font-weight:700;'
            + 'color:#fff;background:#c0392b;border-radius:4px;padding:1px 6px;letter-spacing:0.5px;'
            + 'white-space:nowrap;vertical-align:middle">AUSFALL</span>' : '')
          + '</div>';

        const destDiv = '<div style="position:absolute;left:10px;top:' + destTop + 'px;max-width:240px;'
          + 'font-size:10px;color:rgba(200,221,232,1);overflow:hidden;text-overflow:ellipsis;'
          + 'white-space:nowrap">' + dest + '</div>';

        const timeDiv = '<div style="position:absolute;left:266px;top:50%;transform:translateY(-50%)">'
          + timeInner + '</div>';

        const platDiv = '<div style="position:absolute;right:10px;top:50%;transform:translateY(-50%);'
          + 'text-align:right;font-size:12px;color:rgba(67,152,195,1)">'
          + (d.changed_platform ? '<span style="color:#e57373">&#9888;&nbsp;' + plat + '</span>' : plat)
          + '</div>';

        html += '<div style="position:absolute;left:' + PAD + 'px;right:' + PAD + 'px;top:' + y + 'px;'
          + 'height:' + rowH + 'px;border-radius:8px;border:' + border + ';background:' + bg
          + ';opacity:' + opacity + '">' + badges + destDiv + timeDiv + platDiv + '</div>';

        y += rowH;
      }

      return wrap(html);
    })();
  ]]]

2 „Gefällt mir“

@Technikperry
kann ich mal bitte Code fĂĽr die Sidebar bekommen?
Schaffe es nicht wie bei Dir Uhr und Datum in die Mitte zu bekommen :frowning:

DANKE

kiosk_mode:
  hide_header: '{{ is_state(''input_boolean.vollbild'', ''off'') }}'
  hide_sidebar: '{{ is_state(''input_boolean.vollbild'', ''off'') }}'
sidebar:
  hideTopMenu: false
  hideHassSidebar: false
  width:
    desktop: 22
  digitalClock: true
  digitalClockWithSeconds: false
  clock: false
  date: true
  dateFormat: dddd, DD.MM.YYYY
  sidebarMenu:
    - action: navigate
      navigation_path: /dashboard-ipad/ipad_start
      name: Home
      active: true
      icon: mdi:view-dashboard-outline
    - action: navigate
      navigation_path: /ipad-ug
      name: Untergeschoss
      active: false
      icon: mdi:floor-plan
    - action: navigate
      navigation_path: /ipad-eg
      name: Erdgeschoss
      active: false
      icon: mdi:floor-plan
    - action: navigate
      navigation_path: /ipad-og
      name: Obergeschoss
      active: false
      icon: mdi:floor-plan
    - action: navigate
      navigation_path: /ipad-dg
      name: Dachgeschoss
      active: false
      icon: mdi:floor-plan
    - action: navigate
      navigation_path: /ipad-ab
      name: Aussenbereich
      active: false
      icon: mdi:floor-plan
    - action: navigate
      navigation_path: /ipad-ue
      name: Erweitert
      active: false
      icon: mdi:view-grid-outline
  style: |
    :host {
      background:
        linear-gradient(90deg, transparent 0%, #4398C3 30%, #7EC8E3 50%, #4398C3 70%, transparent 100%) top / 100% 3px no-repeat,
        radial-gradient(circle at 88% 8%, rgba(67,152,195,0.28) 0%, transparent 45%),
        linear-gradient(170deg, #1c3a50 0%, #0e2333 50%, #0a1c2b 100%) !important;
      border-radius: 16px;
      height: 768px !important;
      width: 233px !important;
      margin-left: 14px;
      margin-top: 3px;
      --sidebar-text-color: #E8F4FC;
      box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 0 1px rgba(67,152,195,0.2);
      border: 1px solid rgba(67,152,195,0.25);
      overflow: hidden;
    }

    #customSidebar {
      z-index: 100 !important;
    }
    .sidebarMenu li {
      background-color: transparent !important;
      line-height: 30px !important;
      color: rgba(67,152,195,1) !important;
      font-size: 16px !important;
      font-family: 'EXO 2';
      font-weight: 500 !important;
      letter-spacing: 0.5px;
      text-transform: uppercase;
      margin-left: 1px;
    }
    .sidebarMenu li ha-icon {
      color: rgba(67,152,195,1) !important;
      --mdc-icon-size: 22px;
      margin-right: -10px;
    }
    .sidebarMenu li.active {
      background: linear-gradient(110deg, rgba(30,90,138,0.9) 0%, rgba(15,58,96,0.9) 100%) !important;
      border-radius: 6px !important;
      color: #7EC8E3 !important;
      font-family: 'EXO 2';
      font-weight: 700 !important;
      letter-spacing: 0.5px;
      margin-left: 1px;
      border: 1px solid rgba(67,152,195,0.45) !important;
      box-shadow: 0 0 12px rgba(67,152,195,0.25), inset 0 0 8px rgba(67,152,195,0.08) !important;
    }
    .sidebarMenu li.active ha-icon {
      color: #7EC8E3 !important;
      --mdc-icon-size: 22px;
      filter: drop-shadow(0 0 4px rgba(67,152,195,0.8));
    }
    .digitalClock {
      color: #7EC8E3 !important;
      font-size: 75px !important;
      font-weight: 500;
      font-family: 'EXO 2';
      letter-spacing: 0.5 !important;
      text-align: center;
      margin-top: 25px !important;
      text-shadow: 0 0 10px rgba(67,152,195,0.5);
    }
    .date {
      color: rgba(126,200,227,1);
      font-family: 'EXO 2';
      font-size: 13px;
      font-weight: 500;
      letter-spacing: 0.5px;
      text-align: center;
      text-transform: uppercase;
    }
    div.bottom {
      width: 100% !important;
      margin-left: 10px !important;
      margin-top: 5px !important;
    }
    div > ul {
      border-top: none !important;
      border-bottom: none !important;
      border-image: linear-gradient(90deg, transparent, rgba(67,152,195,0.5), transparent) 1 !important;
      border-top-width: 1px !important;
      border-top-style: solid !important;
      border-bottom-width: 1px !important;
      border-bottom-style: solid !important;
    }

Hi, du findest das unter .date und .digitalClock. Das Geheimnis sind die center aligns.
Die Abmessungen sind auf ein iPad zugeschnitten.

1 „Gefällt mir“

@Technikperry
Könntest du mal den Code für das ganze Dashboard teilen?
Sieht echt spitze aus.

Vielen Dank :blush:

Den kompletten Code kann ich leider nicht teilen. Das Dashboard ist ĂĽber Monate gewachsen und besteht inzwischen aus einer Mischung aus YAML, Kaffee, Verzweiflung und fragwĂĽrdigen Entscheidungen um 23:30 Uhr. :sweat_smile:

Wenn dich eine bestimmte Karte interessiert, helfe ich aber gerne weiter.

4 „Gefällt mir“

danke

1 „Gefällt mir“

Hey, vielen Dank fĂĽr das bereitstellen deiner Arbeit. Sobald ich von meiner Dienstreise zurĂĽck bin, schau ich mir deinen Code genauer an.

GrĂĽĂźe Enrico

1 „Gefällt mir“

ich kenne die problematik des nicht fertig werdens sehr gut. geht mir nämlich auch so. immer wenn ich denke, ich bin fertig, entdecke ich eine card, welche mein bedürfnisse noch besser darstellt und vor allem noch schicker. und zack sitzt man wieder am testen und umbauen.

dein dashboard sieht gut aus. allerdings ist die farbwahl nicht so meins und ich persönlich finde es auch zu erschlagend mit den vielen infos. ich stehe mehr auf masterkacheln, welche beim antippen ein popup öffnen mit weiterführenden infos und einstellmöglichkeiten. finde ich cleaner. aber ist nur meine empfindung.

Hey, schönes Dashboard.

Könntest du bitte den Code für die Karte vom Wasserzähler teilen?

Ja, kann ich machen. Bitte beachte, die Werte stammen von einem Quandify Watergrip und werden nicht über eine Wasseruhr abgelesen. Die Karte wird rot, falls der Watergrip eine Leckage in der Leitung erkennt. Um den Jahresverbrauch zu ermitteln benutze ich die Variable “v_year_start”. Der Tagesverbrauch wird über einen Helfer “sensor.wasserverbrauch_tag” zurückgesetzt.

type: custom:button-card
show_name: false
show_icon: false
show_state: false
triggers_update:
  - sensor.wasserverbrauch_total_volume
  - sensor.wasserverbrauch_water_temperature
  - sensor.wasserverbrauch_water_type
  - sensor.wasserverbrauch_tag
  - binary_sensor.wasserverbrauch_leak
variables:
  v_volume: sensor.wasserverbrauch_total_volume
  v_temp: sensor.wasserverbrauch_water_temperature
  v_type: sensor.wasserverbrauch_water_type
  v_daily: sensor.wasserverbrauch_tag
  v_leak: binary_sensor.wasserverbrauch_leak
  v_year_start: 18.225
styles:
  card:
    - width: 214px
    - height: 377px
    - border-radius: 16px
    - padding: 20px
    - overflow: hidden
    - margin-top: "-3px"
    - margin-left: 1px
    - border: |
        [[[
          return states[variables.v_leak]?.state === 'on'
            ? '1px solid rgba(195,67,85,0.5)'
            : '1px solid rgba(67,152,195,0.25)';
        ]]]
    - background: |
        [[[
          return states[variables.v_leak]?.state === 'on'
            ? 'linear-gradient(145deg, #4a1525 0%, #300a14 40%, #1a050c 100%)'
            : 'linear-gradient(145deg, #1c3a50 0%, #0e2333 40%, #0a1c2b 100%)';
        ]]]
    - box-shadow: |
        [[[
          return states[variables.v_leak]?.state === 'on'
            ? '0 8px 32px rgba(0,0,0,0.5), 0 0 35px rgba(195,67,85,0.45), 0 0 0 1px rgba(195,67,85,0.5)'
            : '0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(67,152,195,0.12)';
        ]]]
    - animation: |
        [[[
          return states[variables.v_leak]?.state === 'on'
            ? 'leak-pulse 1.5s ease-in-out infinite alternate'
            : 'none';
        ]]]
  grid:
    - grid-template-areas: "\"topbar\" \"main\""
    - grid-template-columns: 1fr
    - grid-template-rows: 0px 1fr
    - z-index: "2"
  custom_fields:
    topbar:
      - position: absolute
      - top: "0"
      - left: "0"
      - right: "0"
      - bottom: "0"
      - pointer-events: none
      - z-index: "1"
    main:
      - z-index: "2"
      - width: 100%
      - height: 100%
custom_fields:
  topbar: |
    [[[
      const leak = states[variables.v_leak]?.state === 'on';
      const c1   = leak ? '#C34355' : '#4398C3';
      const c2   = leak ? '#e37e8e' : '#7EC8E3';
      const glow = leak ? 'rgba(195,67,85,0.3)' : 'rgba(67,152,195,0.18)';
      return `
        <div style="position:absolute;top:0;left:0;right:0;height:3px;
          background:linear-gradient(90deg,transparent,${c1},${c2},${c1},transparent);
          z-index:3;pointer-events:none;"></div>
        <div style="position:absolute;top:-20px;right:-20px;
          width:150px;height:150px;border-radius:50%;
          background:radial-gradient(circle,${glow} 0%,transparent 70%);
          pointer-events:none;z-index:1;"></div>`;
    ]]]
  main: |
    [[[
      const inv  = ['unavailable','unknown','none',''];
      const leak = states[variables.v_leak]?.state === 'on';

      // ── Farbschema ────────────────────────────────────────────────
      const accent  = leak ? '#e37e8e'               : '#7EC8E3';
      const accent2 = leak ? 'rgba(227,126,142,1)' : 'rgba(126,200,227,1)';
      const muted   = leak ? 'rgba(195,67,85,1)'   : 'rgba(67,152,195,1)';
      const waveC1  = leak ? '#C34355'                : '#4398C3';
      const waveC2  = leak ? 'rgba(195,67,85,0.35)'   : 'rgba(126,200,227,0.35)';
      const gearC   = leak ? 'rgba(195,67,85,1)'    : 'rgba(67,152,195,1)';
      const glowTxt = leak
        ? 'text-shadow:0 0 20px rgba(195,67,85,0.5);'
        : 'text-shadow:0 0 20px rgba(67,152,195,0.4);';

      // ── Daten laden ───────────────────────────────────────────────
      const volRaw   = states[variables.v_volume]?.state ?? '';
      const tempRaw  = states[variables.v_temp]?.state ?? '';
      const typeRaw  = states[variables.v_type]?.state ?? '';
      const dailyRaw = states[variables.v_daily]?.state ?? '';

      const vol   = parseFloat(volRaw);
      const temp  = parseFloat(tempRaw);
      const daily = parseFloat(dailyRaw);

      // ── Berechnungen ──────────────────────────────────────────────
      const volM3   = isNaN(vol)   ? null : vol / 1000;
      const dailyM3 = isNaN(daily) ? null : daily / 1000;

      const yearTotal = volM3 !== null ? variables.v_year_start + volM3 : null;
      const yearStr   = yearTotal !== null ? yearTotal.toFixed(3) : '—';

      const dailyStr = dailyM3 === null ? '—'
                     : dailyM3 < 1
                       ? Math.round(daily) + ' L'
                       : dailyM3.toFixed(3) + ' mÂł';
      const typeOut = inv.includes((typeRaw||'').toLowerCase()) ? '—' : typeRaw;

      // ── SVG Wasseruhr ─────────────────────────────────────────────
      const waterSvg = `
        <style>
          @keyframes wv1  {from{transform:translateX(0)}to{transform:translateX(-50%)}}
          @keyframes wv2  {from{transform:translateX(0)}to{transform:translateX(-50%)}}
          @keyframes wdrop{
            0%  {opacity:0;transform:translateY(-6px)}
            50% {opacity:0.8}
            100%{opacity:0;transform:translateY(6px)}
          }
          @keyframes cog  {from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
        </style>

        <div style="position:relative;width:140px;height:140px;">

          <svg width="140" height="140" viewBox="0 0 120 120">
            <defs>
              <clipPath id="wclip2"><circle cx="60" cy="60" r="50"/></clipPath>
            </defs>
            <circle cx="60" cy="60" r="52"
              fill="${leak ? 'rgba(195,67,85,0.1)' : 'rgba(67,152,195,0.07)'}"
              stroke="${leak ? 'rgba(195,67,85,0.3)' : 'rgba(67,152,195,0.22)'}"
              stroke-width="1"/>
            <g clip-path="url(#wclip2)">
              <rect x="10" y="48" width="100" height="60" fill="${waveC1}" opacity="0.45"/>
              <g style="animation:wv1 2.5s linear infinite">
                <path d="M0,48 Q15,42 30,48 Q45,54 60,48 Q75,42 90,48 Q105,54 120,48
                         Q135,42 150,48 Q165,54 180,48 Q195,42 210,48 Q225,54 240,48
                         V115 H0 Z" fill="${waveC1}" opacity="0.6"/>
              </g>
              <g style="animation:wv2 3.8s linear infinite reverse">
                <path d="M0,50 Q15,44 30,50 Q45,56 60,50 Q75,44 90,50 Q105,56 120,50
                         Q135,44 150,50 Q165,56 180,50 Q195,44 210,50 Q225,56 240,50
                         V115 H0 Z" fill="${waveC2}"/>
              </g>
            </g>
            <circle cx="60" cy="60" r="52" fill="none"
              stroke="${leak ? 'rgba(195,67,85,0.4)' : 'rgba(67,152,195,0.35)'}"
              stroke-width="1.5"/>
            <circle cx="60" cy="60" r="40" fill="none"
              stroke="${leak ? 'rgba(195,67,85,0.15)' : 'rgba(67,152,195,0.1)'}"
              stroke-width="0.5"/>
            <path d="M60,28 Q71,42 71,51 A11,11 0 0,1 49,51 Q49,42 60,28Z"
              fill="rgba(255,255,255,0.18)" stroke="rgba(255,255,255,0.3)" stroke-width="0.5"/>
            <circle cx="60" cy="52" r="2.5" fill="white" opacity="0.6"
              style="animation:wdrop 1.8s ease-in-out infinite"/>
            <text x="60" y="75" text-anchor="middle"
              font-family="'EXO 2'" font-size="10" font-weight="500"
              fill="${accent}" letter-spacing="0.5">${yearStr}</text>
            <text x="60" y="86" text-anchor="middle"
              font-family="'EXO 2'" font-size="10" font-weight="500"
              fill="${accent2}">mÂł / Jahr</text>
          </svg>

          <!-- Zahnrad als ha-icon ĂĽber dem SVG -->
          <div style="
            position:absolute;bottom:140px;right:-15px;
            width:24px;height:24px;border-radius:50%;
            background:${leak ? 'rgba(195,67,85,0.12)' : 'rgba(67,152,195,0.12)'};
            border:1px solid ${leak ? 'rgba(195,67,85,0.3)' : 'rgba(67,152,195,0.3)'};
            display:flex;align-items:center;justify-content:center;">
            <ha-icon icon="mdi:cog" style="
              --mdc-icon-size:18px;
              color:${gearC};
              animation:cog 3s linear infinite;
              display:block;">
            </ha-icon>
          </div>

        </div>`;

      return `
        <div style="width:100%;height:100%;display:flex;flex-direction:column;">

          <!-- BLOCK 1: HEADER -->
          <div style="display:flex;align-items:center;
            justify-content:space-between;margin-bottom:8px;">
            <div style="display:flex;align-items:center;">
              <span style="font-family:'EXO 2';font-size:16px;font-weight:500;
                letter-spacing:0.5px;text-transform:uppercase;color:${accent};">
                Wasserzähler
              </span>
            </div>
          </div>

          <!-- BLOCK 2: SVG WASSERUHR -->
          <div style="display:flex;justify-content:center;margin-bottom:10px;">
            ${waterSvg}
          </div>

          <!-- BLOCK 3: JAHRESVERBRAUCH -->
          <div style="text-align:center;margin-bottom:7px;">
            <div style="font-family:'EXO 2';font-size:12px;font-weight:500;
              letter-spacing:0.5px;text-transform:uppercase;
              color:${muted};margin-bottom:2px;">
              Jahresverbrauch ${new Date().getFullYear()}
            </div>
            <div style="display:flex;align-items:baseline;justify-content:center;gap:5px;">
              <span style="font-family:'EXO 2';font-size:36px;font-weight:500;
                color:#E8F4FC;letter-spacing:-0.5px;line-height:1;${glowTxt}">
                ${yearStr}
              </span>
              <span style="font-family:'EXO 2';font-size:14px;font-weight:500;
                color:${accent2};">mÂł</span>
            </div>
          </div>

          <!-- TRENNLINIE -->
          <div style="height:1px;margin-bottom:7px;
            background:linear-gradient(90deg,transparent,${leak
              ? 'rgba(195,67,85,0.4)' : 'rgba(67,152,195,0.4)'},transparent);">
          </div>

          <!-- BLOCK 4: TAGESVERBRAUCH -->
          <div style="text-align:center;margin-bottom:7px;">
            <div style="font-family:'EXO 2';font-size:12px;font-weight:500;
              letter-spacing:0.5px;text-transform:uppercase;
              color:${muted};margin-bottom:2px;">
              Tagesverbrauch
            </div>
            <div style="display:flex;align-items:baseline;justify-content:center;gap:5px;">
              <span style="font-family:'EXO 2';font-size:26px;font-weight:500;
                color:${accent};letter-spacing:-0.5px;line-height:1;
                text-shadow:0 0 14px ${waveC1}77;">
                ${dailyStr}
              </span>
            </div>
          </div>

          <!-- TRENNLINIE -->
          <div style="height:1px;margin-bottom:7px;
            background:linear-gradient(90deg,transparent,${leak
              ? 'rgba(195,67,85,0.4)' : 'rgba(67,152,195,0.4)'},transparent);">
          </div>

          <!-- BLOCK 5: FOOTER -->
          <div style="display:flex;align-items:center;justify-content:center;gap:16px;">
            <div style="display:flex;align-items:center;gap:4px;">
              <ha-icon icon="mdi:thermometer-water" style="
                --mdc-icon-size:20px;color:${muted};"></ha-icon>
              <span style="font-family:'EXO 2';font-size:14px;font-weight:500;
                letter-spacing:0.5px;color:${accent2};">
                ${isNaN(temp) ? '—' : temp.toFixed(1)} °C
              </span>
            </div>
            <div style="display:flex;align-items:center;gap:4px;">
              <ha-icon icon="mdi:water-outline" style="
                --mdc-icon-size:14px;color:${muted};"></ha-icon>
              <span style="font-family:'EXO 2';font-size:14px;font-weight:500;
                letter-spacing:0.5px;color:${accent2};">
                ${typeOut}
              </span>
            </div>
          </div>

        </div>`;
    ]]]
extra_styles: |
  @keyframes leak-pulse {
    0%   { box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 20px rgba(195,67,85,0.2); }
    100% { box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 50px rgba(195,67,85,0.7), 0 0 0 1px rgba(195,67,85,0.6); }
  }

Bitte nicht wundern, wenn die Farben mal in HEX und in RGB angegeben sind. Das stammt alles noch aus den Bastelversionen (Optik).
Viel SpaĂź beim nachbauen!

Hier noch ein kleiner Tipp, ich habe mir in Affinity ein Raster mit den Abmessungen meines iPad gebaut. Hier platziere ich dann immer die Karten. Daher auch die Margin Angaben unter card:

1 „Gefällt mir“

Vielen Dank, werde mich demnächst daran versuchen.