From b497820c3002e3ede6ed8dee2bbbce21c9061c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 9 May 2026 10:19:15 +0200 Subject: [PATCH] Retro Crad erstellt, muss aber deutlishc verbessert werden --- .../dist/sternwarte-card-retro.js | 1535 +++++++++++++++++ 1 file changed, 1535 insertions(+) create mode 100644 sternwarte-homeassistant-card/dist/sternwarte-card-retro.js diff --git a/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js b/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js new file mode 100644 index 0000000..58edf42 --- /dev/null +++ b/sternwarte-homeassistant-card/dist/sternwarte-card-retro.js @@ -0,0 +1,1535 @@ +/* Sternwarte Card · Retro Edition + * Vintage Maschinen-Bedienpaneel mit echten 7-Segment-Anzeigen, + * Kippschaltern, Jewel-Lampen, illuminierten Druckknoepfen und + * analoger Zeigerinstrument-Anzeige. Funktional identisch mit der + * Standard-Card, aber komplett unabhaengig. + */ + +const DEFAULT_ENTITIES = { + roof: "cover.sternwarte_dach", + power: "switch.sternwarte_dachsteuerung", + roofClosed: "binary_sensor.sternwarte_dach_zu", + roofOpen: "binary_sensor.sternwarte_dach_offen", + roofMoving: "binary_sensor.sternwarte_dach_bewegt", + roofStatus: "sensor.sternwarte_dachstatus", + roofPosition: "sensor.sternwarte_dachposition", + rain: "binary_sensor.sternwarte_regen", + wind: "sensor.sternwarte_windgeschwindigkeit", + scheduleOpen: "datetime.sternwarte_offnen_geplant", + scheduleClose: "datetime.sternwarte_schliessen_geplant", + astroLat: 50.2984230, + astroLon: 11.0481766, +}; + +const ASTRONOMY_LIB_URL = (() => { + try { return new URL("./astronomy.browser.min.js", import.meta.url).toString(); } + catch { return null; } +})(); + +const MOON_IMAGE_URL = (() => { + try { return new URL("./moon_small.png", import.meta.url).toString(); } + catch { return null; } +})(); + +/* ---------- 7-Segment-SVG ---------- */ + +const SEG_POLYS = { + a: "6,0 24,0 27,3 24,6 6,6 3,3", + b: "27,5 30,8 30,20 27,23 24,20 24,8", + c: "27,29 30,32 30,44 27,47 24,44 24,32", + d: "6,44 24,44 27,47 24,50 6,50 3,47", + e: "3,29 6,32 6,44 3,47 0,44 0,32", + f: "3,5 6,8 6,20 3,23 0,20 0,8", + g: "6,22 24,22 27,25 24,28 6,28 3,25", +}; +const SEG_MAP = { + "0": "abcdef", "1": "bc", "2": "abdeg", "3": "abcdg", "4": "bcfg", + "5": "acdfg", "6": "acdefg", "7": "abc", "8": "abcdefg", "9": "abcdfg", + "-": "g", " ": "", "_": "d", "?": "abeg", +}; + +function seg7(text, opts = {}) { + const { color = "red", size = 1, pad = 0 } = opts; + let str = String(text ?? ""); + if (pad > 0 && str.length < pad) str = str.padStart(pad, " "); + const out = []; + for (const ch of str) { + if (ch === ":") { + out.push(``); + continue; + } + if (ch === ".") { + out.push(``); + continue; + } + const lit = SEG_MAP[ch.toUpperCase()] ?? ""; + const polys = Object.entries(SEG_POLYS).map(([name, points]) => + `` + ).join(""); + out.push(``); + } + return `${out.join("")}`; +} + +/* ---------- Card ---------- */ + +class SternwarteCardRetro extends HTMLElement { + constructor() { + super(); + this._scheduleInputState = { + open: { draft: "", editing: false, pending: false }, + close: { draft: "", editing: false, pending: false }, + }; + this._pendingRender = false; + this._astronomyLoadPromise = null; + this._moonTexture = null; + this._moonTexturePromise = null; + } + + connectedCallback() { + this._ensureAstronomy(); + this._ensureMoonTexture(); + this._ensureClockTimer(); + } + + disconnectedCallback() { + if (this._clockTimer) { window.clearInterval(this._clockTimer); this._clockTimer = null; } + } + + setConfig(config) { + this.config = { ...DEFAULT_ENTITIES, ...config }; + if (!this.shadowRoot) this.attachShadow({ mode: "open" }); + } + + set hass(hass) { this._hass = hass; this._ensureClockTimer(); this._requestRender(); } + + getCardSize() { return 9; } + getGridOptions() { return { rows: 9, columns: 18, min_rows: 8 }; } + + _state(id) { return this._hass?.states?.[id]; } + + _ensureClockTimer() { + if (this._clockTimer) return; + this._clockTimer = window.setInterval(() => { + if (this._hass) this._requestRender(); + }, 1000); + } + + _astronomy() { return window.Astronomy || null; } + + _ensureAstronomy() { + const a = this._astronomy(); + if (a) return Promise.resolve(a); + if (this._astronomyLoadPromise) return this._astronomyLoadPromise; + if (!ASTRONOMY_LIB_URL) return Promise.resolve(null); + + this._astronomyLoadPromise = new Promise((resolve) => { + const finish = () => { + const loaded = this._astronomy(); + if (loaded && this._hass) this._requestRender(); + this._astronomyLoadPromise = null; + resolve(loaded || null); + }; + const existing = Array.from(document.querySelectorAll("script")) + .find((s) => s.src === ASTRONOMY_LIB_URL); + if (existing) { + existing.addEventListener("load", finish, { once: true }); + existing.addEventListener("error", finish, { once: true }); + return; + } + const s = document.createElement("script"); + s.src = ASTRONOMY_LIB_URL; s.async = true; + s.addEventListener("load", finish, { once: true }); + s.addEventListener("error", finish, { once: true }); + document.head.appendChild(s); + }); + return this._astronomyLoadPromise; + } + + _ensureMoonTexture() { + if (this._moonTexture) return Promise.resolve(this._moonTexture); + if (this._moonTexturePromise) return this._moonTexturePromise; + if (!MOON_IMAGE_URL) return Promise.resolve(null); + this._moonTexturePromise = new Promise((resolve) => { + const img = new Image(); + img.addEventListener("load", () => { + this._moonTexture = img; + this._moonTexturePromise = null; + if (this._hass) this._requestRender(); + resolve(img); + }, { once: true }); + img.addEventListener("error", () => { this._moonTexturePromise = null; resolve(null); }, { once: true }); + img.src = MOON_IMAGE_URL; + }); + return this._moonTexturePromise; + } + + _requestRender() { + if (this._isScheduleEditing()) { this._pendingRender = true; return; } + this._pendingRender = false; + this.render(); + } + + _isScheduleEditing() { return Object.values(this._scheduleInputState).some((f) => f.editing); } + _scheduleFieldState(k) { return this._scheduleInputState[k]; } + _scheduleInputValue(k, fb) { const f = this._scheduleFieldState(k); return f.draft || this._isoForInput(fb); } + + _resetScheduleDrafts() { + for (const f of Object.values(this._scheduleInputState)) { + f.draft = ""; f.editing = false; f.pending = false; + } + } + + _beginScheduleEdit(k, fb) { + const f = this._scheduleFieldState(k); + f.editing = true; f.pending = false; + if (!f.draft) f.draft = this._isoForInput(fb); + } + _updateScheduleDraft(k, v) { const f = this._scheduleFieldState(k); f.draft = v; f.pending = false; } + _endScheduleEdit(k) { + const f = this._scheduleFieldState(k); f.editing = false; + if (!this._isScheduleEditing() && this._pendingRender) this._requestRender(); + } + _bindScheduleInput(k, id, fb) { + const i = this.shadowRoot.getElementById(id); if (!i) return; + i.addEventListener("focus", () => this._beginScheduleEdit(k, fb)); + i.addEventListener("input", (e) => this._updateScheduleDraft(k, e.target.value)); + i.addEventListener("change", (e) => this._updateScheduleDraft(k, e.target.value)); + i.addEventListener("blur", () => this._endScheduleEdit(k)); + } + + _firstExistingEntityId(ids) { + for (const id of ids) if (id && this._state(id)) return id; + return ids.find(Boolean) || null; + } + + _scheduleOpenEntityId() { + return this._firstExistingEntityId([ + this.config.scheduleOpen, + "datetime.sternwarte_offnen_geplant", + "datetime.sternwarte_oeffnen_geplant", + ]); + } + _scheduleCloseEntityId() { + return this._firstExistingEntityId([this.config.scheduleClose, "datetime.sternwarte_schliessen_geplant"]); + } + _powerEntityId() { + const c = this.config?.power; + if (c && this._state(c)) return c; + const states = this._hass?.states || {}; + const fb = Object.keys(states).find((id) => { + if (!id.startsWith("switch.")) return false; + const n = id.toLowerCase(); + const fn = String(states[id]?.attributes?.friendly_name || "").toLowerCase(); + return n.includes("dachsteuer") || (n.includes("sternwarte") && n.includes("power")) || fn.includes("dachsteuerung"); + }); + return fb || c || "switch.sternwarte_dachsteuerung"; + } + + _call(d, s) { try { Promise.resolve(this._hass.callService(d, s, {})).catch(() => {}); } catch {} } + _callEntity(d, s, id) { + if (!id) return; + try { Promise.resolve(this._hass.callService(d, s, { entity_id: id })).catch(() => {}); } catch {} + } + + _bindClick(id, fn) { + this.shadowRoot.getElementById(id)?.addEventListener("click", (e) => { + e.preventDefault(); e.stopPropagation(); fn(); + }); + } + + _showMoreInfo(id) { + if (!id) return; + const ev = new Event("hass-more-info", { bubbles: true, composed: true }); + ev.detail = { entityId: id }; + this.dispatchEvent(ev); + } + _bindMoreInfo(id, entityId) { + const el = this.shadowRoot.getElementById(id); + if (!el || !entityId) return; + const open = (e) => { e.preventDefault(); e.stopPropagation(); this._showMoreInfo(entityId); }; + el.addEventListener("click", open); + el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") open(e); }); + } + + _setDateTime(eid, inputId, key) { + const i = this.shadowRoot.getElementById(inputId); + const f = this._scheduleFieldState(key); + const v = f?.draft || i?.value || ""; + if (!v || !eid) return; + f.pending = true; f.editing = false; f.draft = v; + this._hass.callService("datetime", "set_value", { entity_id: eid, datetime: v }); + if (this._pendingRender) this._requestRender(); + } + + _isoForInput(state) { + if (!state || !state.state || state.state === "unknown" || state.state === "unavailable") return ""; + const v = new Date(state.state); if (Number.isNaN(v.getTime())) return ""; + const p = (n) => String(n).padStart(2, "0"); + return `${v.getFullYear()}-${p(v.getMonth() + 1)}-${p(v.getDate())}T${p(v.getHours())}:${p(v.getMinutes())}`; + } + + _seg7DateTime(state) { + if (!state || !state.state || state.state === "unknown" || state.state === "unavailable") return "--:--"; + const v = new Date(state.state); if (Number.isNaN(v.getTime())) return "--:--"; + const p = (n) => String(n).padStart(2, "0"); + return `${p(v.getDate())}.${p(v.getMonth() + 1)} ${p(v.getHours())}:${p(v.getMinutes())}`; + } + + _roofPositionValue() { + const v = Number(this._state(this.config.roofPosition)?.state); + if (Number.isNaN(v)) return null; + return Math.max(0, Math.min(100, Math.round(v))); + } + + _windValue() { + const w = this._state(this.config.wind); + const v = Number(w?.state); + if (Number.isNaN(v)) return { num: null, unit: "km/h", level: 0 }; + let level = 0; + if (v > 15) level = 2; else if (v > 10) level = 1; + return { num: Math.round(v), unit: w?.attributes?.unit_of_measurement ?? "km/h", level }; + } + + /* --- astronomy / moon (unchanged from original) --- */ + + _isMESZ(d) { + const y = d.getUTCFullYear(); + const lastSunday = (m, h) => { + const e = new Date(Date.UTC(y, m + 1, 0, h)); + while (e.getUTCDay() !== 0) e.setUTCDate(e.getUTCDate() - 1); + return e; + }; + return d >= lastSunday(2, 1) && d < lastSunday(9, 1); + } + _astroObserver() { + const a = this._astronomy(); if (!a) return null; + const lat = Number(this.config.astroLat), lon = Number(this.config.astroLon); + if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; + return new a.Observer(lat, lon, 0); + } + _astroTimeToUtcHour(t) { + if (!t) return null; + const d = t.date instanceof Date ? t.date : null; + if (!d || Number.isNaN(d.getTime())) return null; + return d.getUTCHours() + d.getUTCMinutes() / 60 + d.getUTCSeconds() / 3600; + } + _calcTwilight(date) { + const a = this._astronomy(), o = this._astroObserver(); + const empty = { sun: { dawn: null, dusk: null }, civil: { dawn: null, dusk: null }, naut: { dawn: null, dusk: null }, astro: { dawn: null, dusk: null } }; + if (!a || !o) return empty; + try { + const body = a.Body?.Sun ?? "Sun"; + const start = new Date(date); start.setHours(0, 0, 0, 0); + const limit = 2; + const ev = (alt) => ({ + dawn: this._astroTimeToUtcHour(a.SearchAltitude(body, o, +1, start, limit, alt)), + dusk: this._astroTimeToUtcHour(a.SearchAltitude(body, o, -1, start, limit, alt)), + }); + return { + sun: { dawn: this._astroTimeToUtcHour(a.SearchRiseSet(body, o, +1, start, limit, 0)), dusk: this._astroTimeToUtcHour(a.SearchRiseSet(body, o, -1, start, limit, 0)) }, + civil: ev(-6), naut: ev(-12), astro: ev(-18), + }; + } catch { return empty; } + } + _moonStateAt(d) { + const a = this._astronomy(), o = this._astroObserver(); + if (!a || !o) return { altitude: -Infinity, illumination: 0, phaseAngle: 0 }; + try { + const body = a.Body?.Moon ?? "Moon"; + const eq = a.Equator(body, d, o, true, true); + const hz = a.Horizon(d, o, eq.ra, eq.dec, "normal"); + const ill = a.Illumination(body, d); + const ph = Number(a.MoonPhase(d)) || 0; + return { + altitude: (Number(hz?.altitude) || 0) * Math.PI / 180, + illumination: Math.max(0, Math.min(1, Number(ill?.phase_fraction) || 0)), + phaseAngle: ((ph % 360) + 360) % 360, + }; + } catch { return { altitude: -Infinity, illumination: 0, phaseAngle: 0 }; } + } + _moonVisualState() { + const m = this._moonStateAt(new Date()); + const ph = m.phaseAngle ?? 0; + const waxing = ph <= 180; + const r = ph * Math.PI / 180; + return { + illumination: Math.max(0, Math.min(1, m.illumination || 0)), + illuminationPercent: Math.round((m.illumination || 0) * 100), + waxing, phaseAngle: ph, cosPhase: Math.cos(r), + }; + } + _renderMoonPhaseDataUrl(mv, size = 64) { + const tx = this._moonTexture; if (!tx) return ""; + const c = document.createElement("canvas"); c.width = size; c.height = size; + const ctx = c.getContext("2d"); if (!ctx) return ""; + const r = size / 2 - 1, x = size / 2, y = size / 2; + const cp = Number(mv.cosPhase) || 0; + ctx.save(); ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.clip(); + ctx.fillStyle = "rgba(8,10,16,0.96)"; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); + ctx.globalAlpha = 0.98; ctx.drawImage(tx, x - r, y - r, r * 2, r * 2); ctx.globalAlpha = 1; + ctx.fillStyle = "rgba(8,10,16,0.96)"; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); + ctx.save(); ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.clip(); + if (mv.illumination >= 0.999) { + ctx.drawImage(tx, x - r, y - r, r * 2, r * 2); + } else if (mv.illumination > 0.001) { + ctx.beginPath(); + for (let i = 0; i <= 72; i++) { + const t = -r + (2 * r * i) / 72; + const xr = Math.sqrt(Math.max(0, r * r - t * t)); + const xt = (mv.waxing ? cp : -cp) * xr; + if (i === 0) ctx.moveTo(x + xt, y + t); else ctx.lineTo(x + xt, y + t); + } + for (let i = 72; i >= 0; i--) { + const t = -r + (2 * r * i) / 72; + const xr = Math.sqrt(Math.max(0, r * r - t * t)); + ctx.lineTo(x + (mv.waxing ? xr : -xr), y + t); + } + ctx.closePath(); ctx.clip(); + ctx.drawImage(tx, x - r, y - r, r * 2, r * 2); + } + ctx.restore(); ctx.restore(); + ctx.strokeStyle = "rgba(255,200,80,0.35)"; ctx.lineWidth = 1; + ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.stroke(); + return c.toDataURL("image/png"); + } + + _astroState() { + const now = new Date(); + const tw = this._calcTwilight(now); + const off = this._isMESZ(now) ? 2 : 1; + const shift = (h) => ((h - 12 + 24) % 24); + const localize = (e) => e ? { dawn: (e.dawn + off + 24) % 24, dusk: (e.dusk + off + 24) % 24 } : null; + const lt = { sun: localize(tw.sun), civil: localize(tw.civil), naut: localize(tw.naut), astro: localize(tw.astro) }; + const nowL = shift(now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600); + const segs = []; + const colors = { night: "#0a1320", astro: "#23426d", naut: "#3d6aaf", civil: "#b87a3a", day: "#e8b94c" }; + const add = (s, e, k) => { + if (s === null || e === null) return; + const ss = shift(s), ee = shift(e); + if (ee < ss) { segs.push({ start: ss, end: 24, color: colors[k] }); segs.push({ start: 0, end: ee, color: colors[k] }); return; } + if (ee <= ss) return; + segs.push({ start: ss, end: ee, color: colors[k] }); + }; + add(0, lt.astro?.dawn, "night"); + add(lt.astro?.dawn, lt.naut?.dawn, "astro"); + add(lt.naut?.dawn, lt.civil?.dawn, "naut"); + add(lt.civil?.dawn, lt.sun?.dawn, "civil"); + add(lt.sun?.dawn, lt.sun?.dusk, "day"); + add(lt.sun?.dusk, lt.civil?.dusk, "civil"); + add(lt.civil?.dusk, lt.naut?.dusk, "naut"); + add(lt.naut?.dusk, lt.astro?.dusk, "astro"); + add(lt.astro?.dusk, 24, "night"); + if (!segs.length) add(0, 24, "night"); + segs.sort((a, b) => a.start - b.start); + const stops = segs.flatMap((s) => { + const a = `${(s.start / 24) * 100}%`, b = `${(s.end / 24) * 100}%`; + return [`${s.color} ${a}`, `${s.color} ${b}`]; + }); + const grad = stops.length ? `linear-gradient(90deg, ${stops.join(", ")})` : colors.night; + const toLocal = (st) => { + if (!st?.state || st.state === "unknown" || st.state === "unavailable") return null; + const v = new Date(st.state); if (Number.isNaN(v.getTime())) return null; + return shift(v.getHours() + v.getMinutes() / 60); + }; + return { + gradient: grad, + markerLeft: `${(nowL / 24) * 100}%`, + openMarkerLeft: toLocal(this._state(this._scheduleOpenEntityId())), + closeMarkerLeft: toLocal(this._state(this._scheduleCloseEntityId())), + }; + } + + /* ---------- render ---------- */ + + render() { + if (!this.shadowRoot || !this._hass) return; + + const powerEid = this._powerEntityId(); + const powerState = this._state(powerEid)?.state; + const powerOn = powerState === "on"; + const powerOff = powerState === "off"; + + const rainOn = this._state(this.config.rain)?.state === "on"; + const movingOn = this._state(this.config.roofMoving)?.state === "on"; + const openOn = this._state(this.config.roofOpen)?.state === "on"; + const closedOn = this._state(this.config.roofClosed)?.state === "on"; + + const openSchedEid = this._scheduleOpenEntityId(); + const closeSchedEid = this._scheduleCloseEntityId(); + const openSched = this._state(openSchedEid); + const closeSched = this._state(closeSchedEid); + const openInputV = this._scheduleInputValue("open", openSched); + const closeInputV = this._scheduleInputValue("close", closeSched); + if (openInputV === this._isoForInput(openSched)) { + this._scheduleFieldState("open").draft = ""; + this._scheduleFieldState("open").pending = false; + } + if (closeInputV === this._isoForInput(closeSched)) { + this._scheduleFieldState("close").draft = ""; + this._scheduleFieldState("close").pending = false; + } + + const pos = this._roofPositionValue(); + const wind = this._windValue(); + const moonV = this._moonVisualState(); + const moonImg = this._renderMoonPhaseDataUrl(moonV, 96); + const astro = this._astroState(); + + let openDis, closeDis, stopDis; + if (!powerOn) { openDis = closeDis = stopDis = true; } + else if (movingOn) { openDis = closeDis = true; stopDis = false; } + else if (openOn && !closedOn) { openDis = true; closeDis = false; stopDis = true; } + else if (closedOn && !openOn) { openDis = false; closeDis = true; stopDis = true; } + else if (closedOn && openOn) { openDis = closeDis = true; stopDis = false; } + else { openDis = closeDis = stopDis = false; } + + const now = new Date(); + const pad = (n) => String(n).padStart(2, "0"); + const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; + const dateStr = `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${String(now.getFullYear()).slice(-2)}`; + + /* analog gauge for roof position */ + const gaugeAngle = pos === null ? -135 : -135 + (pos / 100) * 270; + const positionStr = pos === null ? " --" : String(pos).padStart(3, " "); + const windStr = wind.num === null ? " --" : String(wind.num).padStart(3, " "); + const moonStr = String(moonV.illuminationPercent).padStart(3, " "); + + /* status string for main display */ + let statusStr = "READY"; + if (!powerOn && powerOff) statusStr = " STBY"; + else if (movingOn) statusStr = " RUN "; + else if (openOn && !closedOn) statusStr = "OPEN "; + else if (closedOn && !openOn) statusStr = "SHUT "; + else if (rainOn) statusStr = "RAIN "; + + const css = this._renderStyles(); + + this.shadowRoot.innerHTML = ` + + +
+
+
+ +
+
SONNENWEG OBSERVATORY · DOME CONTROL UNIT MK−II
+
+
SER. № 1966−A
+
+
+
+ + ${rainOn ? ` +
+ + ⚠ REGEN ERKANNT · DACH GESPERRT ⚠ + +
+ ` : ""} + +
+ +
+
PRIMARY READOUT
+
+
+ UHR + ${seg7(timeStr, { color: "red", size: 0.75 })} +
+
+ DAT + ${seg7(dateStr, { color: "amber", size: 0.55 })} +
+
+ POS + ${seg7(positionStr, { color: "red", size: 0.75 })} + % +
+
+ WND + ${seg7(windStr, { color: wind.level === 2 ? "red" : wind.level === 1 ? "amber" : "green", size: 0.75 })} + ${wind.unit} +
+
+ MND + ${seg7(moonStr, { color: "amber", size: 0.55 })} + % +
+
+ STA + ${seg7(statusStr, { color: powerOn ? "green" : "amber", size: 0.55 })} +
+
+
+ + +
+
+
DOME STATUS
+
+ ${this._jewel("DACH ZU", "green", closedOn)} + ${this._jewel("OFFEN", "green", openOn)} + ${this._jewel("BEWEGT", "amber", movingOn, true)} + ${this._jewel("REGEN", "red", rainOn, true)} + ${this._jewel("POWER", "white", powerOn)} + ${this._jewel("FEHLER", "red", powerState === undefined || powerState === "unavailable")} +
+
+ +
+
POSITION
+ +
+
+ + +
+
MOON SCOPE
+
+
+ ${moonImg ? `Mond` : `
`} +
+
+
+
+ ${seg7(moonStr, { color: "amber", size: 0.5 })} + % ILL +
+
+
+ + +
+
DIURNAL CHART · 12 → 12
+
+
+
+ ${astro.openMarkerLeft === null ? "" : `
`} + ${astro.closeMarkerLeft === null ? "" : `
`} +
+
+ 12 + 18 + 00 + 06 + 12 +
+
+
+ + +
+
+
MASTER POWER
+
+
+
+
+
+
+
+
+
+ EIN + AUS +
+
+
+
+ ${seg7(powerOn ? " ON" : powerOff ? "OFF" : "---", { color: powerOn ? "green" : "red", size: 0.5 })} +
+
+
+ +
+
DOME COMMAND
+
+ + + +
+
+
+ + +
+
SCHEDULE PROGRAMMER
+
+
+ ÖFFNEN +
${seg7(this._seg7DateTime(openSched), { color: "amber", size: 0.45 })}
+ + +
+
+ SCHLIESSEN +
${seg7(this._seg7DateTime(closeSched), { color: "amber", size: 0.45 })}
+ + +
+
+ + + +
+
+
+ +
+ SONNENWEG · SCHWARZENBACH AM WALD + 50.298° N · 11.048° E + SW−CTRL/2.0 — RETRO PANEL +
+
+
+ `; + + this._bindClick("open-btn", () => this._callEntity("cover", "open_cover", this.config.roof)); + this._bindClick("stop-btn", () => this._callEntity("cover", "stop_cover", this.config.roof)); + this._bindClick("close-btn", () => this._callEntity("cover", "close_cover", this.config.roof)); + this._bindClick("nautical-btn", () => this._call("sternwarte", "set_nautical_schedule")); + this._bindClick("clear-schedule-btn", () => { + this._resetScheduleDrafts(); + this._call("sternwarte", "clear_schedule"); + this._requestRender(); + }); + this._bindClick("open-schedule-btn", () => this._setDateTime(openSchedEid, "open-schedule-input", "open")); + this._bindClick("close-schedule-btn", () => this._setDateTime(closeSchedEid, "close-schedule-input", "close")); + this._bindMoreInfo("wind-btn", this.config.wind); + this._bindMoreInfo("pos-gauge", this.config.roofPosition); + this._bindMoreInfo("moon-scope", this.config.roofStatus); + this._bindScheduleInput("open", "open-schedule-input", openSched); + this._bindScheduleInput("close", "close-schedule-input", closeSched); + + /* power toggle */ + const tog = this.shadowRoot.getElementById("power-toggle"); + if (tog) { + const flip = () => { + const svc = powerOn ? "turn_off" : "turn_on"; + this._callEntity("switch", svc, powerEid); + }; + tog.addEventListener("click", (e) => { e.preventDefault(); flip(); }); + tog.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); flip(); } + }); + } + } + + _jewel(label, color, on, blink = false) { + return ` +
+
+
+
+
${label}
+
+ `; + } + + _gaugeTicks() { + let s = ""; + for (let i = 0; i <= 10; i++) { + const ang = -135 + (i / 10) * 270; + const major = i % 5 === 0; + const r1 = 75, r2 = major ? 64 : 69; + const rad = ang * Math.PI / 180; + const x1 = 100 + r1 * Math.sin(rad), y1 = 110 - r1 * Math.cos(rad); + const x2 = 100 + r2 * Math.sin(rad), y2 = 110 - r2 * Math.cos(rad); + s += ``; + } + return s; + } + + _renderStyles() { + return ` + @import url('https://fonts.googleapis.com/css2?family=Special+Elite&family=Share+Tech+Mono&display=swap'); + + :host { + display: block; + font-family: 'Share Tech Mono', 'Courier New', monospace; + } + ha-card { + background: transparent; + border: none; + box-shadow: none; + overflow: hidden; + } + + /* === overall console === */ + .console { + position: relative; + padding: 22px 22px 18px; + border-radius: 14px; + color: #f1e4c5; + background: + radial-gradient(circle at 30% 20%, rgba(255,235,180,0.06), transparent 60%), + radial-gradient(circle at 80% 90%, rgba(0,0,0,0.35), transparent 70%), + repeating-linear-gradient(90deg, rgba(255,255,255,0.025) 0 1px, transparent 1px 3px), + linear-gradient(160deg, #4d5a3e 0%, #3b4731 35%, #2c361f 100%); + box-shadow: + inset 0 0 0 2px #1c2210, + inset 0 0 0 4px #6b7a55, + inset 0 0 60px rgba(0,0,0,0.5), + 0 8px 24px rgba(0,0,0,0.45); + text-shadow: 0 1px 0 rgba(0,0,0,0.4); + } + .rivets { + position: absolute; + width: 14px; height: 14px; + border-radius: 50%; + background: + radial-gradient(circle at 30% 30%, #f1ebd2 0%, #8a8064 35%, #2a2515 75%, #000 100%); + box-shadow: + inset -1px -1px 2px rgba(0,0,0,0.7), + 0 1px 2px rgba(0,0,0,0.6); + } + .r-tl { top: 8px; left: 8px; } + .r-tr { top: 8px; right: 8px; } + .r-bl { bottom: 8px; left: 8px; } + .r-br { bottom: 8px; right: 8px; } + + /* === header / dymo === */ + .header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + } + .dymo { + font-family: 'Special Elite', 'Courier New', serif; + font-size: 14px; + letter-spacing: 0.18em; + color: #2a200f; + background: + repeating-linear-gradient(90deg, rgba(255,255,255,0.06) 0 1px, transparent 1px 2px), + linear-gradient(180deg, #d8c98a 0%, #bba85f 100%); + padding: 8px 18px; + border-radius: 4px; + border: 1px solid #6b5a25; + box-shadow: + inset 0 1px 0 rgba(255,255,255,0.4), + inset 0 -2px 0 rgba(0,0,0,0.2), + 0 2px 4px rgba(0,0,0,0.45); + text-shadow: none; + } + .header-right { + display: flex; align-items: center; gap: 12px; + } + .serial { + font-size: 10px; + letter-spacing: 0.25em; + opacity: 0.6; + font-family: 'Special Elite', serif; + } + .pwr-led { + width: 14px; height: 14px; + border-radius: 50%; + background: #2a0a0a; + box-shadow: + inset 0 0 0 2px #1a1a1a, + inset 0 0 4px rgba(0,0,0,0.8), + 0 0 0 3px #4a3f1f; + } + .pwr-led.on { + background: radial-gradient(circle at 30% 30%, #fff 0%, #6cf06c 40%, #1f8a1f 100%); + box-shadow: + inset 0 0 0 2px #1a1a1a, + inset 0 0 4px rgba(0,0,0,0.4), + 0 0 12px rgba(108,240,108,0.7), + 0 0 0 3px #4a3f1f; + } + .pwr-led.off { + background: radial-gradient(circle at 30% 30%, #ffd9d9 0%, #c43030 50%, #4a0000 100%); + box-shadow: + inset 0 0 0 2px #1a1a1a, + inset 0 0 4px rgba(0,0,0,0.4), + 0 0 8px rgba(196,48,48,0.5), + 0 0 0 3px #4a3f1f; + } + + /* === alarm strip === */ + .alarm-strip { + display: flex; align-items: center; justify-content: center; + gap: 14px; + margin-bottom: 14px; + padding: 8px 14px; + background: + repeating-linear-gradient(45deg, #1a1305 0 12px, #5a2c0c 12px 24px); + border: 2px solid #1a1305; + border-radius: 4px; + animation: alarm-flash 1s linear infinite; + } + .alarm-text { + font-weight: 700; + font-size: 14px; + letter-spacing: 0.25em; + color: #ffeacc; + } + .alarm-lamp { + width: 14px; height: 14px; + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, #fff 0%, #ff5a30 50%, #6a1500 100%); + box-shadow: 0 0 12px rgba(255,90,48,0.9); + animation: alarm-flash 0.7s linear infinite alternate; + } + @keyframes alarm-flash { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.55; } + } + + /* === bezels (inset metal panels) === */ + .bezel { + position: relative; + padding: 22px 14px 14px; + border-radius: 6px; + background: + repeating-linear-gradient(90deg, rgba(255,255,255,0.03) 0 1px, transparent 1px 2px), + linear-gradient(160deg, #2a2418 0%, #1b170e 100%); + box-shadow: + inset 0 2px 4px rgba(0,0,0,0.7), + inset 0 -1px 0 rgba(255,255,255,0.05), + 0 1px 0 rgba(255,255,255,0.06); + border: 1px solid #0a0805; + } + .bezel-label { + position: absolute; + top: -8px; left: 12px; + padding: 2px 8px; + font-family: 'Special Elite', serif; + font-size: 10px; + letter-spacing: 0.22em; + color: #f1e4c5; + background: linear-gradient(180deg, #6b7a55 0%, #4d5a3e 100%); + border: 1px solid #1c2210; + border-radius: 2px; + box-shadow: 0 1px 2px rgba(0,0,0,0.5); + text-shadow: 0 1px 0 rgba(0,0,0,0.4); + } + + /* === rows === */ + .row { display: grid; gap: 14px; margin-bottom: 14px; } + .row-displays { + grid-template-columns: minmax(0, 2.2fr) minmax(0, 1.4fr) minmax(0, 1fr); + } + .row-controls { + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + } + .col-stack { display: grid; grid-template-rows: auto auto; gap: 14px; } + + /* === 7-seg === */ + .seg7 { + display: inline-flex; + align-items: flex-end; + gap: 2px; + padding: 4px 6px; + --d7-base-w: 30px; + --d7-base-h: 50px; + } + .d7, .d7-colon, .d7-dot { + width: calc(var(--d7-base-w) * var(--d7-size, 1)); + height: calc(var(--d7-base-h) * var(--d7-size, 1)); + display: inline-block; + } + .seg { + fill: rgba(0,0,0,0.5); + opacity: 0.10; + transition: opacity 80ms linear; + } + .seg.on { opacity: 1; } + .col-red .seg.on { + fill: #ff2a18; + filter: drop-shadow(0 0 2px #ff5538) drop-shadow(0 0 4px rgba(255,40,20,0.6)); + } + .col-amber .seg.on { + fill: #ffaa1a; + filter: drop-shadow(0 0 2px #ffc04a) drop-shadow(0 0 4px rgba(255,170,30,0.6)); + } + .col-green .seg.on { + fill: #4cff60; + filter: drop-shadow(0 0 2px #80ff90) drop-shadow(0 0 4px rgba(76,255,96,0.5)); + } + .col-red .dot { fill: #ff2a18; filter: drop-shadow(0 0 2px #ff5538); } + .col-amber .dot { fill: #ffaa1a; filter: drop-shadow(0 0 2px #ffc04a); } + .col-green .dot { fill: #4cff60; filter: drop-shadow(0 0 2px #80ff90); } + + /* === display panel === */ + .display-bezel { + background: + repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 3px), + linear-gradient(160deg, #100a04 0%, #1c130a 100%); + } + .display-grid { + display: grid; + gap: 6px; + } + .seg-row { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 10px; + background: + radial-gradient(ellipse at center, rgba(255,255,255,0.03) 0%, transparent 70%), + #050200; + border-radius: 3px; + box-shadow: + inset 0 2px 6px rgba(0,0,0,0.9), + inset 0 0 0 1px #2a1f0a; + } + .seg-cap { + font-family: 'Special Elite', serif; + font-size: 10px; + letter-spacing: 0.2em; + color: #c5a960; + min-width: 36px; + } + .seg-unit { + font-family: 'Special Elite', serif; + font-size: 11px; + letter-spacing: 0.15em; + color: #c5a960; + margin-left: auto; + } + + /* === lamps === */ + .lamp-bezel { + background: + repeating-linear-gradient(45deg, rgba(255,255,255,0.02) 0 2px, transparent 2px 4px), + linear-gradient(160deg, #2c2618 0%, #1b170e 100%); + } + .lamps { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px 6px; + } + .jewel { + display: flex; flex-direction: column; align-items: center; gap: 4px; + } + .jewel-bezel { + width: 38px; height: 38px; + border-radius: 50%; + background: + radial-gradient(circle at 30% 30%, #fff 0%, #c0b89a 25%, #5a4f30 70%, #1a1408 100%); + padding: 4px; + box-shadow: + inset 0 1px 2px rgba(255,255,255,0.4), + inset 0 -2px 4px rgba(0,0,0,0.6), + 0 2px 4px rgba(0,0,0,0.6); + } + .jewel-lens { + width: 100%; height: 100%; + border-radius: 50%; + position: relative; + } + .jewel-lens::after { + content: ""; + position: absolute; top: 4px; left: 6px; + width: 8px; height: 6px; + border-radius: 50%; + background: rgba(255,255,255,0.45); + filter: blur(1px); + } + .lens-green.off { background: radial-gradient(circle at 35% 35%, #2d4a30 0%, #0c1a0e 70%); } + .lens-green.on { background: radial-gradient(circle at 35% 35%, #c4ffc8 0%, #4cff60 35%, #1a8a28 100%); box-shadow: 0 0 16px rgba(76,255,96,0.7); } + .lens-amber.off { background: radial-gradient(circle at 35% 35%, #4a3a14 0%, #1a1408 70%); } + .lens-amber.on { background: radial-gradient(circle at 35% 35%, #fff5c4 0%, #ffc14a 35%, #b07b10 100%); box-shadow: 0 0 16px rgba(255,193,74,0.7); } + .lens-red.off { background: radial-gradient(circle at 35% 35%, #4a1414 0%, #1a0808 70%); } + .lens-red.on { background: radial-gradient(circle at 35% 35%, #ffd0d0 0%, #ff4030 35%, #8a0a0a 100%); box-shadow: 0 0 16px rgba(255,64,48,0.75); } + .lens-white.off { background: radial-gradient(circle at 35% 35%, #3a3528 0%, #14110a 70%); } + .lens-white.on { background: radial-gradient(circle at 35% 35%, #fff 0%, #fce8b4 50%, #b08020 100%); box-shadow: 0 0 16px rgba(255,232,180,0.7); } + .blink { animation: lamp-blink 0.9s linear infinite; } + @keyframes lamp-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } + + .jewel-label { + font-family: 'Special Elite', serif; + font-size: 9px; + letter-spacing: 0.18em; + color: #d8c98a; + text-align: center; + } + + /* === gauge === */ + .gauge-bezel { + background: + radial-gradient(ellipse at 50% 0%, rgba(255,255,255,0.04), transparent 60%), + linear-gradient(160deg, #1f1a0e 0%, #100c06 100%); + cursor: pointer; + outline: none; + } + .gauge-bezel:focus-visible { box-shadow: inset 0 2px 4px rgba(0,0,0,0.7), 0 0 0 2px #c5a960; } + .gauge { width: 100%; height: auto; display: block; } + + /* === moon scope === */ + .moon-bezel { + display: flex; flex-direction: column; align-items: center; gap: 8px; + cursor: pointer; + } + .scope { + width: 96px; height: 96px; + border-radius: 50%; + padding: 6px; + background: + radial-gradient(circle at 30% 30%, #f1ebd2 0%, #8a8064 30%, #2a2515 80%, #000 100%); + box-shadow: + inset 0 2px 4px rgba(255,255,255,0.4), + inset 0 -3px 6px rgba(0,0,0,0.7), + 0 3px 6px rgba(0,0,0,0.6); + } + .scope-glass { + width: 100%; height: 100%; + border-radius: 50%; + position: relative; + overflow: hidden; + background: #050810; + box-shadow: inset 0 0 0 2px #1a1408; + } + .moon-img { + width: 100%; height: 100%; + display: block; + border-radius: 50%; + } + .moon-img.placeholder { + background: radial-gradient(circle at 30% 30%, #aaa 0%, #444 60%, #111 100%); + } + .scope-cross { + position: absolute; inset: 0; + background: + linear-gradient(90deg, transparent 49.5%, rgba(255,200,80,0.18) 49.5% 50.5%, transparent 50.5%), + linear-gradient(0deg, transparent 49.5%, rgba(255,200,80,0.18) 49.5% 50.5%, transparent 50.5%); + pointer-events: none; + } + .scope-readout { + display: flex; align-items: center; gap: 4px; + } + + /* === twilight strip === */ + .twilight-bezel { + background: + linear-gradient(160deg, #1f1a0e 0%, #100c06 100%); + } + .strip { + position: relative; + height: 38px; + border-radius: 3px; + overflow: hidden; + box-shadow: + inset 0 2px 4px rgba(0,0,0,0.7), + inset 0 0 0 1px #2a1f0a; + } + .strip-paper { + position: absolute; inset: 0; + opacity: 0.92; + } + .strip-grid { + position: absolute; inset: 0; + background: + repeating-linear-gradient(90deg, transparent 0 calc(100%/24 - 1px), rgba(0,0,0,0.35) calc(100%/24 - 1px) calc(100%/24)), + repeating-linear-gradient(0deg, transparent 0 8px, rgba(255,255,255,0.04) 8px 9px); + pointer-events: none; + } + .strip-mark { + position: absolute; + top: -2px; bottom: -2px; + width: 3px; + margin-left: -1px; + z-index: 2; + box-shadow: 0 0 6px currentColor; + } + .strip-mark.open { background: #4cff60; color: rgba(76,255,96,0.8); } + .strip-mark.close { background: #ffaa1a; color: rgba(255,170,26,0.8); } + .strip-now { + position: absolute; + top: -3px; bottom: -3px; + width: 4px; + margin-left: -2px; + background: #ff2a18; + box-shadow: 0 0 8px rgba(255,42,24,0.85); + z-index: 3; + } + .strip-ticks { + position: absolute; inset: 0; + pointer-events: none; + } + .strip-ticks span { + position: absolute; + top: 50%; transform: translate(-50%, -50%); + font-family: 'Special Elite', serif; + font-size: 10px; + color: rgba(255,255,255,0.85); + text-shadow: 0 0 3px rgba(0,0,0,0.8); + } + + /* === toggle switch (bat-handle) === */ + .toggle-bezel { + display: flex; flex-direction: column; align-items: center; + } + .toggle-area { + position: relative; + display: flex; flex-direction: column; align-items: center; gap: 8px; + padding-top: 6px; + } + .toggle-frame { + position: relative; + width: 70px; height: 130px; + display: flex; align-items: center; justify-content: center; + } + .toggle-base { + position: absolute; + top: 50%; left: 50%; + transform: translate(-50%, -50%); + width: 46px; height: 46px; + border-radius: 50%; + background: + radial-gradient(circle at 30% 30%, #f1ebd2 0%, #8a8064 35%, #1a1408 100%); + box-shadow: + inset 0 1px 2px rgba(255,255,255,0.5), + inset 0 -3px 6px rgba(0,0,0,0.6), + 0 3px 6px rgba(0,0,0,0.6); + } + .toggle-base::after { + content: ""; + position: absolute; + top: 50%; left: 50%; + transform: translate(-50%, -50%); + width: 16px; height: 16px; + border-radius: 50%; + background: radial-gradient(circle, #1a1408 0%, #000 100%); + box-shadow: inset 0 2px 3px rgba(0,0,0,0.9); + } + .toggle-handle { + position: absolute; + top: 50%; left: 50%; + width: 14px; height: 70px; + margin-left: -7px; + margin-top: -35px; + transform-origin: 50% 50%; + transition: transform 220ms cubic-bezier(0.5, 1.6, 0.4, 1); + } + .toggle.up .toggle-handle { transform: translateY(-22px) rotate(0deg); } + .toggle.down .toggle-handle { transform: translateY(22px) rotate(180deg); } + .toggle-handle::before { + content: ""; + position: absolute; + left: 50%; top: 30px; + width: 10px; height: 40px; + margin-left: -5px; + background: linear-gradient(90deg, #4a4a4a 0%, #c8c8c8 35%, #f4f4f4 50%, #c8c8c8 65%, #4a4a4a 100%); + border-radius: 2px; + box-shadow: 0 0 4px rgba(0,0,0,0.5); + } + .toggle-tip { + position: absolute; + left: 50%; top: 0; + width: 22px; height: 30px; + margin-left: -11px; + background: + radial-gradient(ellipse at 35% 30%, #fff 0%, #d8d8d8 30%, #6a6a6a 70%, #2a2a2a 100%); + border-radius: 50% 50% 45% 45%; + box-shadow: + inset 0 -3px 6px rgba(0,0,0,0.5), + 0 2px 4px rgba(0,0,0,0.6); + } + .toggle-marks { + position: absolute; inset: 0; + pointer-events: none; + } + .toggle-marks .mk { + position: absolute; + left: 50%; transform: translateX(-50%); + font-family: 'Special Elite', serif; + font-size: 11px; + letter-spacing: 0.2em; + color: #d8c98a; + } + .toggle-marks .mk.top { top: 4px; color: #4cff60; } + .toggle-marks .mk.bot { bottom: 4px; color: #ff6040; } + .toggle { cursor: pointer; outline: none; } + .toggle:focus-visible .toggle-base { box-shadow: 0 0 0 3px #c5a960, inset 0 1px 2px rgba(255,255,255,0.5), inset 0 -3px 6px rgba(0,0,0,0.6); } + + /* guard bracket */ + .guard { + position: absolute; + top: -2px; left: 50%; transform: translateX(-50%); + width: 60px; height: 110px; + border: 3px solid #c43030; + border-bottom: none; + border-radius: 30px 30px 0 0; + box-shadow: + inset 0 0 0 1px rgba(255,255,255,0.15), + 0 2px 4px rgba(0,0,0,0.5); + background: transparent; + pointer-events: none; + transition: transform 280ms ease; + z-index: 2; + } + .guard.lifted { + transform: translateX(-50%) translateY(-12px) rotateX(-65deg); + transform-origin: 50% 100%; + } + .power-stat { + text-align: center; + } + + /* === push buttons === */ + .buttons { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 14px; + align-items: center; + justify-items: center; + padding: 8px 0; + } + .pbtn { + position: relative; + width: 90px; height: 90px; + border-radius: 50%; + border: none; + cursor: pointer; + font-family: 'Special Elite', serif; + font-size: 11px; + letter-spacing: 0.2em; + color: #fff; + text-shadow: 0 1px 2px rgba(0,0,0,0.7); + background: + radial-gradient(circle at 30% 30%, var(--btn-hi, #fff) 0%, var(--btn-mid, #888) 35%, var(--btn-lo, #222) 100%); + box-shadow: + inset 0 2px 4px rgba(255,255,255,0.4), + inset 0 -4px 8px rgba(0,0,0,0.6), + 0 0 0 6px #1c1610, + 0 0 0 7px #6b5a25, + 0 5px 10px rgba(0,0,0,0.6); + transition: transform 80ms ease, box-shadow 80ms ease; + } + .pbtn:active:not(:disabled) { + transform: translateY(2px); + box-shadow: + inset 0 4px 8px rgba(0,0,0,0.6), + inset 0 -1px 2px rgba(255,255,255,0.2), + 0 0 0 6px #1c1610, + 0 0 0 7px #6b5a25, + 0 2px 4px rgba(0,0,0,0.6); + } + .pbtn:disabled { + cursor: not-allowed; + filter: grayscale(0.7) brightness(0.55); + } + .pbtn-cap { + display: block; + margin-top: 4px; + font-weight: 700; + } + .pbtn-green { + --btn-hi: #d8ffd8; --btn-mid: #4cd060; --btn-lo: #0a4a18; + box-shadow: + inset 0 2px 4px rgba(255,255,255,0.5), + inset 0 -4px 8px rgba(0,0,0,0.5), + 0 0 0 6px #1c1610, + 0 0 0 7px #6b5a25, + 0 0 18px rgba(76,208,96,0.45), + 0 5px 10px rgba(0,0,0,0.6); + } + .pbtn-amber { + --btn-hi: #fff0c8; --btn-mid: #e69a2a; --btn-lo: #5a3208; + box-shadow: + inset 0 2px 4px rgba(255,255,255,0.5), + inset 0 -4px 8px rgba(0,0,0,0.5), + 0 0 0 6px #1c1610, + 0 0 0 7px #6b5a25, + 0 0 18px rgba(230,154,42,0.45), + 0 5px 10px rgba(0,0,0,0.6); + } + .pbtn-mushroom { + width: 110px; height: 110px; + --btn-hi: #ffdada; --btn-mid: #d62020; --btn-lo: #4a0606; + box-shadow: + inset 0 3px 6px rgba(255,255,255,0.5), + inset 0 -6px 12px rgba(0,0,0,0.6), + 0 0 0 8px #1c1610, + 0 0 0 9px #c43030, + 0 0 22px rgba(214,32,32,0.55), + 0 6px 12px rgba(0,0,0,0.7); + } + .pbtn-mushroom .pbtn-cap { + font-size: 13px; + letter-spacing: 0.3em; + } + .pbtn-mushroom:disabled { + filter: grayscale(0.5) brightness(0.6); + } + + /* === schedule programmer === */ + .sched-bezel { + background: + repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0 1px, transparent 1px 3px), + linear-gradient(160deg, #1f1a0e 0%, #100c06 100%); + } + .sched-grid { + display: grid; gap: 10px; + } + .sched-row { + display: grid; + grid-template-columns: 90px minmax(0, 1fr) minmax(150px, 200px) 70px; + gap: 10px; + align-items: center; + } + .sched-cap { + font-family: 'Special Elite', serif; + font-size: 11px; + letter-spacing: 0.18em; + color: #c5a960; + } + .sched-readout { + padding: 4px 8px; + background: #050200; + border-radius: 3px; + box-shadow: + inset 0 2px 4px rgba(0,0,0,0.9), + inset 0 0 0 1px #2a1f0a; + min-height: 28px; + display: flex; align-items: center; + } + input[type="datetime-local"] { + width: 100%; + box-sizing: border-box; + padding: 7px 10px; + border-radius: 3px; + border: 1px solid #2a1f0a; + background: + repeating-linear-gradient(0deg, transparent 0 4px, rgba(255,255,255,0.03) 4px 5px), + #1a1408; + color: #f1e4c5; + font-family: 'Share Tech Mono', monospace; + font-size: 13px; + letter-spacing: 0.05em; + box-shadow: inset 0 2px 3px rgba(0,0,0,0.6); + } + input[type="datetime-local"]::-webkit-calendar-picker-indicator { + filter: invert(0.85) sepia(1) saturate(2) hue-rotate(-10deg); + cursor: pointer; + } + .kbtn { + font-family: 'Special Elite', serif; + font-size: 11px; + letter-spacing: 0.18em; + padding: 9px 12px; + border: 1px solid #1a1408; + border-radius: 3px; + background: + linear-gradient(180deg, #d8c98a 0%, #a89559 50%, #76632a 100%); + color: #1a1408; + cursor: pointer; + box-shadow: + inset 0 1px 0 rgba(255,255,255,0.5), + inset 0 -2px 0 rgba(0,0,0,0.25), + 0 2px 3px rgba(0,0,0,0.5); + text-shadow: 0 1px 0 rgba(255,255,255,0.3); + transition: transform 80ms ease, box-shadow 80ms ease; + } + .kbtn:active { + transform: translateY(1px); + box-shadow: + inset 0 2px 3px rgba(0,0,0,0.4), + inset 0 -1px 0 rgba(255,255,255,0.15), + 0 1px 1px rgba(0,0,0,0.5); + } + .kbtn-secondary { + background: linear-gradient(180deg, #6b7a55 0%, #4d5a3e 50%, #2c361f 100%); + color: #f1e4c5; + text-shadow: 0 1px 0 rgba(0,0,0,0.4); + } + .kbtn-info { + background: linear-gradient(180deg, #5a8ac4 0%, #3d6aaf 50%, #234783 100%); + color: #f1e4c5; + text-shadow: 0 1px 0 rgba(0,0,0,0.4); + } + .sched-actions { + display: flex; gap: 10px; flex-wrap: wrap; + margin-top: 4px; + } + + /* === footer === */ + .footer { + margin-top: 16px; + padding-top: 10px; + display: flex; + justify-content: space-between; + gap: 12px; + font-family: 'Special Elite', serif; + font-size: 10px; + letter-spacing: 0.2em; + color: rgba(241,228,197,0.55); + border-top: 1px dashed rgba(241,228,197,0.2); + flex-wrap: wrap; + } + + /* === responsive === */ + @media (max-width: 880px) { + .row-displays { grid-template-columns: 1fr; } + .row-controls { grid-template-columns: 1fr; } + .lamps { grid-template-columns: repeat(3, 1fr); } + .sched-row { + grid-template-columns: 1fr; + } + } + @media (max-width: 520px) { + .console { padding: 16px; } + .dymo { font-size: 11px; padding: 6px 12px; letter-spacing: 0.12em; } + .lamps { grid-template-columns: repeat(2, 1fr); } + .pbtn { width: 72px; height: 72px; font-size: 9px; } + .pbtn-mushroom { width: 90px; height: 90px; } + .buttons { gap: 8px; } + } + `; + } +} + +if (!customElements.get("sternwarte-card-retro")) { + customElements.define("sternwarte-card-retro", SternwarteCardRetro); +} + +window.customCards = window.customCards || []; +if (!window.customCards.some((c) => c?.type === "sternwarte-card-retro")) { + window.customCards.push({ + type: "sternwarte-card-retro", + name: "Sternwarte Card · Retro", + description: "Vintage Maschinen-Bedienpaneel mit 7-Segment-Anzeigen, Kippschaltern und beleuchteten Druckknöpfen", + }); +}