From 14782c86aab2d1285c41b821ec55677972d5c3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Thu, 3 Sep 2026 09:12:27 +0200 Subject: [PATCH] v0.11.4 --- Cargo.lock | 2 +- Cargo.toml | 2 +- FILE_MANIFEST.sha256 | 36 +- README.md | 2 +- docs/API.md | 6 +- .../gree_controller/manifest.json | 2 +- lang/en.json | 34 +- lang/pl.json | 38 +- mock/index.html | 1302 ++++ mock/mock.js | 28 + mock/styles.css | 6763 +++++++++++++++++ mock/theme-init.js | 9 + web/index.html | 77 +- web/js/charts.js | 72 +- web/js/core.js | 2 +- web/js/entities.js | 14 +- web/js/events.js | 4 + web/js/flows.js | 123 +- web/js/navigation.js | 2 +- web/js/settings-ui.js | 18 +- web/styles.css | 339 +- 21 files changed, 8793 insertions(+), 82 deletions(-) create mode 100644 mock/index.html create mode 100644 mock/mock.js create mode 100644 mock/styles.css create mode 100644 mock/theme-init.js diff --git a/Cargo.lock b/Cargo.lock index 1c81672..4ba64db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,7 +633,7 @@ dependencies = [ [[package]] name = "gree-controller" -version = "0.11.3" +version = "0.11.4" dependencies = [ "aes", "aes-gcm", diff --git a/Cargo.toml b/Cargo.toml index 6e0bffd..5df72c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gree-controller" -version = "0.11.3" +version = "0.11.4" edition = "2021" authors = ["GREE Controller contributors"] description = "Standalone local GREE HVAC controller with Web UI, SQLite and Home Assistant sensor support" diff --git a/FILE_MANIFEST.sha256 b/FILE_MANIFEST.sha256 index a1595ac..a6cb044 100644 --- a/FILE_MANIFEST.sha256 +++ b/FILE_MANIFEST.sha256 @@ -1,11 +1,11 @@ d67af429e4da9ce08e9d2f2a8472849ffbd70d135b1c5da535a076026794d04c ./.env.example a4ec3874a2e3ab1bad28fb40bb620f7b01f64d01ad9b699306bf70ada31227db ./.gitignore -2b03135cfb84c2df0f14380231e954882d743ba922c65ba3cbc56474057a039b ./Cargo.lock -2bd686728663c50fa901cc3b35519da0e91e08e44994aff46783dedbd62409bd ./Cargo.toml +ee9f0a05e85c3ffc081647716020ddfa26dfe128bc955eaed56a3b57cbbb81e6 ./Cargo.lock +2cb81c4e01143414768f0592c4e362056ecf04678c465503789b6162fefa700c ./Cargo.toml 19b2943504acb8f8de280f873a8dbec4bb6ebbe3870b158f5655d4fb8c298f5f ./LICENSE -fe279b20b6e9da85c1e639078c213875bfa842102017cda4d0d06120fe8fe3ba ./README.md +a2713e3390c232ccf94e310ba18cb18ac47156ec050260f697fbc4257a9e1239 ./README.md 41dfdc6d099b54f87d4dd51f696122b3c88d3bf3ddf420bea8f4bc621ba920b0 ./build.rs -ec33deef383b0aee070d3fef68c54387d6dba4fc2fc9b56f4a902484c5338989 ./docs/API.md +14188e547cd09ec63240cee480cf499ca479f300653fdf24dbef90a7175e4ec5 ./docs/API.md 2e1e18fd8167dabfe2469c26e85cce62486c7cb6a502c63c6f6b0cd74d5885e0 ./docs/FLOW.md a0893b2a56eb1523f1a72871842e9be2139a5fafba1f51ae942fc407a6e4ca34 ./home-assistant/README.md f8e8559fe10fe523ac5bc9aac25c6e26e862f679d502e8f3c39f38a0a8e40911 ./home-assistant/custom_components/gree_controller/__init__.py @@ -17,7 +17,7 @@ ab08fbe40e9bb48ebcbfff98760aaa0c9434b61b705aaaadea0c09c255d99b0e ./home-assista 5a96fe8f5c035c34f1339370270cd078056202d09e236dec75735be11de92a7d ./home-assistant/custom_components/gree_controller/entity_map.py c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/icon.png c4fb75c246db651087900ebfc2291ff41ac87652cd6194fc0b776b0005c1cbcf ./home-assistant/custom_components/gree_controller/logo.png -629c10dd8b65cfde3a1db168cd6b5b642a87c4a4cd1149cc77121a121e525bbb ./home-assistant/custom_components/gree_controller/manifest.json +1168f40e21d3c21341353f254f0b79157133e9625f9cdcd552bf6bcfd444e7c9 ./home-assistant/custom_components/gree_controller/manifest.json 38d659d0273c0d1428679e44574a2666c605d1b3796d323b626baa40560788cf ./home-assistant/custom_components/gree_controller/number.py 39c4309001b75abb56234f05662bc06e077054986876f1927937edbce528ec95 ./home-assistant/custom_components/gree_controller/select.py cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assistant/custom_components/gree_controller/sensor.py @@ -26,9 +26,13 @@ cca65482e36d48035aca178121a378fe7d578d600acff267ae81a6399c0da653 ./home-assista 13f30e2dcdcedbd1b6c3f99c2335e0487108fd72c8e86922368b84f2fa2038ae ./home-assistant/custom_components/gree_controller/translations/pl.json 4513070521d3dda0efb0d974a86ba674494cfb2b66fe9e5cac5b1b0430dede97 ./home-assistant/generated/gree_controller_entities.example.json 253a0bc912786e67ea7fc92a64e4a510ad973bec343a88ccfb1f28fca3e8cf01 ./lang/README.md -69f9a7d4152036db8a48773d5c0fa364ab0fbb9cde1f28d07390023509d3544d ./lang/en.json -36cbb4ab3233b21ef13ce1dd01f747c5067d406f246df8c52ea48b3a426d64f3 ./lang/pl.json +7414e94d61d2dfd9734ee92a2a1423cae67e91e8733c213098b9bcb6a2885a22 ./lang/en.json +1ff6dab25d8a29d971cd10de8d0df1b6173670ba1e6f65c0c669bf6b134bcf23 ./lang/pl.json d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba ./make_zip.py +1e7b1bcf4194abfb8bad0d601268af0547d58541731e98b74a3500d7629d0be0 ./mock/index.html +007c78e0cb404778985980b23bb5f8bbc7a8f04c80015204e24c321970e3b15e ./mock/mock.js +75999d965ea56a2cd961afe5771e6e9b1be513fab1dcafb480fbe9c0c232e7df ./mock/styles.css +d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./mock/theme-init.js b14233a8987e53bbbdd6770386ba10fa166ec25c1e098275c173c544b37846fc ./presets/bedroom_window_night.json 1960841119c0b673fb2f03621b32237552fd828045304cd221401214b997c910 ./presets/device_resilience.json c5276fecc6a33c5de912e5a73b0a829d4e18c18d57b9c0ee49179ef96f133fc7 ./presets/dual_threshold_control.json @@ -172,24 +176,24 @@ f13f1be3d5789539eba3fc1b59c14835f8c681eca7634e118fa3763a53aeee2b ./src/queries/ b92a6cb158b494fe145b43c7641e65f6fafff47201d7d76edbec2cfd8b94835c ./systemd/gree-controller.service 544a31cb2e5374227026afd6abd0d910381f3fca1638154ebd74e3c3d96a35d1 ./web/404.html e98bdd7204349cce1ec6f57283509697af0bbc72280622a6c3efa6fed242db4f ./web/favicon.svg -b39ebf9538c078f2311d3064207da0fa46217037e1eb56566867028565719e53 ./web/index.html +4f273e189008df110cd3cad1d323e5f07e2651f805419b427811d89fdd04245f ./web/index.html 9ba3f6fa05b72aa989139b2a909982571b2a02055052e4c40104f1e81a9aa7eb ./web/js/README.md 21b662914590e720dafefe4dc3dccc91dbeb963cce9ae91f007c835d2d0c1fe5 ./web/js/bootstrap.js -72fa280996ba331a05bdb29a1f49b4e61bdd8964953c8ce1db952156620f08fe ./web/js/charts.js -782ce96f3ec006c447e8c44c60ce4142e0fd830704a6ef0511a66505c12cb6bf ./web/js/core.js +dee7c4292889ddd721c220e86fae909f0d662b585e6d9c5d9e2f8fa85d9ed2ad ./web/js/charts.js +87064e7d504dc2afbc1aa3a1926fcec0cc80b31e2a6a6d931d7af7fc4f519b20 ./web/js/core.js dfc123885a9b6b5c1d07c12d2d2625ff15aa0844e5cd3736092ccd0a360da39c ./web/js/dashboard.js -f53e2da5c48b55cf0191ffeb970a2c053c66e62d29099e3c5b5fccc8d89440cc ./web/js/entities.js -9a0e87911f78cd8a969f4c57ce74774f5fa829fdecd79494d9a7f9d5804f9792 ./web/js/events.js -2bcf1292d26815e4919492c558601d8f0ddd61163f7ec916b562a40b409abdde ./web/js/flows.js +3f5b9a4d62c994edded0ce24fb9ea45141d935a1928d4f68fb67f8ca287bafe1 ./web/js/entities.js +cdd11ab0fac33b7b1749b8db4e30486e9d0258b65fe54fd56c3a520b2b72b2d7 ./web/js/events.js +d4aea1d6ec595000eaddca3b6f0e3d4003901365ece7788147a6ed207cffcc78 ./web/js/flows.js f06557c6d338259059b990f8f79c91d68ac8ec1f245c302468f08929b5506901 ./web/js/forms.js dbf36223863ba882c03eccea4516ba8db7acf0c2e72c6c64490cb994f1e20f96 ./web/js/history.js 7eaa04992ea828dc89f5eaebf674cbbfd160f53eb8d0d6ef0d4e47bd8c28c435 ./web/js/main.js -7f23507b2f1f632d69b466688f68f3f0bf661dc6856a1ed14a462e063114a980 ./web/js/navigation.js +3dcfc24f238ad1310155707268b60c5a415f2a8977ceba48c7e695b7135d5705 ./web/js/navigation.js c56059693d09e35261dc5dd1940289ffc67c82ea7c547750d57e6c79fe75b05b ./web/js/realtime.js c8c82e88c3715b1bfabf155e36266a4c94f5e2fc03eb39dcdd9d08a1985a997c ./web/js/router.js -34513d9ec2e45c60443dffe028eea74f415b8a29016df6af3377a80a4ac0df5b ./web/js/settings-ui.js +bdeb55546a1dff5e5bdfde4b114d6330f025df1b62e528bf18a93ad6dd4c3e83 ./web/js/settings-ui.js aa6a2ba22648de44547efd09f41c4335ebe38ed564f4684e942d7827df40f1a2 ./web/js/settings.js 6b653e4fe2d4db4ffae6ff8f39b8ce57a10a990dc0000ee4aae51d949afadd52 ./web/manifest.webmanifest -3069a9860857d4c9f1d820c6066cc9c9326d495b49083a422d327249032dd9e4 ./web/styles.css +75999d965ea56a2cd961afe5771e6e9b1be513fab1dcafb480fbe9c0c232e7df ./web/styles.css ae1b03f30b494f474a781a5d32072f1e73eba2b7c768109fc1cc8028cb6662a4 ./web/sw.js d505d793ce7cc9485b45b78bba1c0d51887adc7451ab59a42702946e5b991382 ./web/theme-init.js diff --git a/README.md b/README.md index 4cb2207..abd051e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Self-hosted controller for GREE-compatible air conditioners with a local Web UI, thermostat zones, schedules, Home Assistant integration, history, notifications and a documented HTTP/WebSocket API. -**Current release: 0.11.3** +**Current release: 0.11.4** > [Full API reference](docs/API.md) — authentication, every endpoint, request bodies, response models, WebSocket events and examples. diff --git a/docs/API.md b/docs/API.md index dc110ee..cbf8f38 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # GREE Controller API reference -HTTP and WebSocket API for GREE Controller **0.11.3**. +HTTP and WebSocket API for GREE Controller **0.11.4**. [← Main documentation](../README.md) @@ -213,7 +213,7 @@ Response: { "status": "ok", "name": "gree-controller", - "version": "0.11.3", + "version": "0.11.4", "uptime_seconds": 1234, "control_ready": true, "time": "2026-08-30T06:54:00Z" @@ -237,7 +237,7 @@ Returns the initial Web UI snapshot: "settings": {}, "outdoor_temperature": null, "system": { - "version": "0.11.3", + "version": "0.11.4", "uptime_seconds": 1234, "auth_required": false, "control_ready": true, diff --git a/home-assistant/custom_components/gree_controller/manifest.json b/home-assistant/custom_components/gree_controller/manifest.json index a9d0339..efb9918 100644 --- a/home-assistant/custom_components/gree_controller/manifest.json +++ b/home-assistant/custom_components/gree_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "gree_controller", "name": "GREE Controller", - "version": "0.11.1", + "version": "0.11.4", "config_flow": true, "integration_type": "hub", "iot_class": "local_polling", diff --git a/lang/en.json b/lang/en.json index e39df91..cd7de8b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -474,7 +474,7 @@ "settings.eventRetentionDays": "Event/log retention (days)", "settings.debugHint": "Live diagnostics displayed on every application page.", "settings.sensorAliases": "Sensor aliases", - "settings.sensorAliasesHint": "Friendly names are used only in charts and selectors. Home Assistant requests always use the original entity_id.", + "settings.sensorAliasesHint": "Friendly names are used in charts and selectors. Badges show whether an entity is collected as metrics, used by Flow, or both. A shared Flow input by itself does not enable metric collection.", "settings.aliasPlaceholder": "Friendly name", "settings.noSensorAliases": "No Home Assistant sensors are known yet. Add an entity_id below.", "settings.aliasRequired": "Enter both entity_id and alias.", @@ -730,6 +730,8 @@ "history.zoomOut": "Zoom out", "history.zoomReset": "Reset zoom", "history.zoomIn": "Zoom in", + "history.fullscreen": "Enlarge chart", + "history.exitFullscreen": "Close enlarged chart", "house.powerOffConfirm": "Turn all air conditioners off? All thermostats will remain locally OFF with no timer. You can later enable an individual thermostat, a group, or the whole house.", "zones.temporaryStarting": "Starting…", "notifications.applicationTokenPlaceholder": "Application token", @@ -943,7 +945,7 @@ "flow.sharedInputType": "Source type", "flow.sharedInputAdd": "Add input", "flow.sharedInputsTitle": "Shared Flow inputs", - "flow.sharedInputsHint": "Define reusable value sources once, then use them in many Flows. Operators and comparison values are configured only in Flow blocks.", + "flow.sharedInputsHint": "Define reusable value sources once, then use them in many Flows. Operators and comparison values are configured only in Flow blocks. HA sensors added only here are not stored as metrics.", "flow.sharedInputsEmpty": "No shared inputs", "flow.sharedInputsEmptyHint": "Add a constant, Home Assistant state, or another shared source for multiple Flows.", "flow.sharedInputNodeHint": "This block uses a central value source. The comparison below belongs only to this Flow.", @@ -1056,6 +1058,12 @@ "flow.durationSeconds": "Duration [s]", "flow.stableForHint": "The input condition must remain true continuously for the configured duration. State survives restarts.", "flow.serviceData": "Service data (JSON)", + "flow.serviceExample": "Example: to turn on switch.gas use domain = switch, service = turn_on and entity_id = switch.gas.", + "flow.enabledState": "Enabled", + "flow.disabledState": "Disabled", + "flow.enabledSelect": "Flow state", + "settings.sensorMetricBadge": "Metrics", + "settings.sensorFlowBadge": "Flow", "flow.invalidJson": "Invalid JSON", "flow.mean": "Mean", "flow.median": "Median", @@ -1101,6 +1109,26 @@ "flow.draftDisabledHint": "A draft is always disabled. Finish the Flow and save it again to create executable tasks.", "flow.draftCannotEnable": "Finish and save the Flow first. A draft cannot be enabled.", "flow.saveAsDraftConfirm": "The Flow cannot be saved as executable:\n\n{reason}\n\nSave it as a draft instead? The draft will be disabled and will not create any tasks or schedules.", - "flow.savedAsDraft": "Flow saved as a disabled draft. You can return to it later." + "flow.savedAsDraft": "Flow saved as a disabled draft. You can return to it later.", + "automationHub.navigation": "Automation section", + "nav.sectionControl": "Control", + "nav.sectionAutomation": "Automation", + "nav.sectionData": "Data & system", + "nav.homeAssistantShort": "Home Assistant", + "actions.more": "More options", + "flow.addBlock": "Add block", + "flow.searchBlocks": "Search blocks…", + "flow.noBlocksFound": "No matching blocks", + "flow.noBlocksFoundHint": "Change the search phrase or clear the filter.", + "flow.zoomControls": "Flow canvas zoom", + "flow.zoomOut": "Zoom out", + "flow.zoomIn": "Zoom in", + "flow.fitView": "Fit", + "flow.unsavedChanges": "Unsaved changes", + "flow.savedState": "Saved", + "flow.expandSettings": "Expand block settings", + "flow.notSavedYet": "Not saved yet", + "schedules.enabledState": "Enabled", + "schedules.disabledState": "Disabled" } } diff --git a/lang/pl.json b/lang/pl.json index 24e2dd5..a5384c8 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -474,7 +474,7 @@ "settings.eventRetentionDays": "Retencja zdarzeń/logów (dni)", "settings.debugHint": "Diagnostyka na żywo wyświetlana na każdej podstronie aplikacji.", "settings.sensorAliases": "Aliasy czujników", - "settings.sensorAliasesHint": "Przyjazne nazwy są używane tylko na wykresach i w selektorach. Zapytania do Home Assistanta zawsze używają oryginalnego entity_id.", + "settings.sensorAliasesHint": "Przyjazne nazwy są używane na wykresach i w selektorach. Oznaczenia pokazują, czy encja trafia do metryk, jest używana we Flow, czy w obu miejscach. Samo wspólne wejście Flow nie uruchamia zapisu metryk.", "settings.aliasPlaceholder": "Przyjazna nazwa", "settings.noSensorAliases": "Nie wykryto jeszcze czujników Home Assistant. Możesz dodać entity_id poniżej.", "settings.aliasRequired": "Podaj entity_id oraz alias.", @@ -730,6 +730,8 @@ "history.zoomOut": "Pomniejsz wykres", "history.zoomReset": "Przywróć powiększenie", "history.zoomIn": "Powiększ wykres", + "history.fullscreen": "Powiększ wykres", + "history.exitFullscreen": "Zamknij powiększony wykres", "house.powerOffConfirm": "Wyłączyć wszystkie klimatyzatory? Wszystkie termostaty pozostaną lokalnie WYŁ. bez timera. Później możesz włączyć pojedynczy termostat, grupę albo cały dom.", "zones.temporaryStarting": "Uruchamianie…", "notifications.applicationTokenPlaceholder": "Token aplikacji", @@ -814,7 +816,7 @@ "zones.queueCancelAllConfirm": "Anulować wszystkie oczekujące zadania ({count}) ochrony sprężarki?", "nav.flows": "Flow", "flow.eyebrow": "Logika wizualna", - "flow.new": "Nowy Flow", + "flow.new": "Nowe flow", "flow.infoTitle": "Jedno miejsce dla harmonogramów i automatyzacji", "flow.infoText": "Układasz logikę z bloków. GREE Controller tłumaczy Flow na generowane harmonogramy i automatyzacje. Wygenerowane wpisy są tylko do odczytu poza tym edytorem.", "flow.sourceOfTruth": "Flow = źródło logiki", @@ -932,7 +934,7 @@ "flow.listCount": "{count} Flow", "flow.quickToggleHint": "Szybko włącz lub wyłącz Flow bez otwierania edytora", "flow.quickEnabled": "Flow włączony", - "flow.quickDisabled": "Flow wyłączony", + "flow.quickDisabled": "Flow wyłączone", "flow.selectAll": "Zaznacz wszystko", "flow.clearSelection": "Wyczyść zaznaczenie", "flow.selectedCount": "Zaznaczono: {count}", @@ -943,7 +945,7 @@ "flow.sharedInputType": "Typ źródła", "flow.sharedInputAdd": "Dodaj wejście", "flow.sharedInputsTitle": "Wspólne wejścia Flow", - "flow.sharedInputsHint": "Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow. Operatory i wartości porównań ustawia się wyłącznie w blokach Flow.", + "flow.sharedInputsHint": "Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow. Operatory i wartości porównań ustawia się wyłącznie w blokach Flow. Czujniki HA dodane tylko tutaj nie są zapisywane do metryk.", "flow.sharedInputsEmpty": "Brak wspólnych wejść", "flow.sharedInputsEmptyHint": "Dodaj stałą, stan Home Assistant albo inne współdzielone źródło dla wielu Flow.", "flow.sharedInputNodeHint": "Ten blok korzysta z centralnego źródła wartości. Poniższe porównanie należy tylko do tego Flow.", @@ -1056,6 +1058,12 @@ "flow.durationSeconds": "Czas [s]", "flow.stableForHint": "Warunek na wejściu musi pozostać prawdziwy bez przerwy przez podany czas. Stan jest zachowywany po restarcie.", "flow.serviceData": "Dane usługi (JSON)", + "flow.serviceExample": "Przykład: aby włączyć switch.gas ustaw domain = switch, service = turn_on i entity_id = switch.gas.", + "flow.enabledState": "Włączone", + "flow.disabledState": "Wyłączone", + "flow.enabledSelect": "Stan Flow", + "settings.sensorMetricBadge": "Metryki", + "settings.sensorFlowBadge": "Flow", "flow.invalidJson": "Nieprawidłowy JSON", "flow.mean": "Średnia", "flow.median": "Mediana", @@ -1101,6 +1109,26 @@ "flow.draftDisabledHint": "Szkic jest zawsze wyłączony. Dokończ Flow i zapisz go ponownie, aby utworzyć zadania wykonawcze.", "flow.draftCannotEnable": "Najpierw dokończ i zapisz Flow. Szkicu nie można włączyć.", "flow.saveAsDraftConfirm": "Flow nie może zostać zapisany jako wykonywalny:\n\n{reason}\n\nZapisać go jako szkic? Szkic będzie wyłączony i nie utworzy żadnych zadań ani harmonogramów.", - "flow.savedAsDraft": "Flow zapisany jako wyłączony szkic. Możesz wrócić do niego później." + "flow.savedAsDraft": "Flow zapisany jako wyłączony szkic. Możesz wrócić do niego później.", + "automationHub.navigation": "Sekcja automatyzacji", + "nav.sectionControl": "Sterowanie", + "nav.sectionAutomation": "Automatyzacja", + "nav.sectionData": "Dane i system", + "nav.homeAssistantShort": "Home Assistant", + "actions.more": "Więcej opcji", + "flow.addBlock": "Dodaj blok", + "flow.searchBlocks": "Szukaj bloków…", + "flow.noBlocksFound": "Brak pasujących bloków", + "flow.noBlocksFoundHint": "Zmień wyszukiwaną frazę lub wyczyść filtr.", + "flow.zoomControls": "Powiększenie obszaru Flow", + "flow.zoomOut": "Pomniejsz", + "flow.zoomIn": "Powiększ", + "flow.fitView": "Dopasuj", + "flow.unsavedChanges": "Niezapisane zmiany", + "flow.savedState": "Zapisano", + "flow.expandSettings": "Rozwiń ustawienia bloku", + "flow.notSavedYet": "Jeszcze nie zapisano", + "schedules.enabledState": "Włączony", + "schedules.disabledState": "Wyłączony" } } diff --git a/mock/index.html b/mock/index.html new file mode 100644 index 0000000..4e65858 --- /dev/null +++ b/mock/index.html @@ -0,0 +1,1302 @@ + + + + + + + + + + + + + GREE Controller + + + +
+
+
GREE Controller
+
+
0/0
+
+
+ + + +
+
+ + + +
+ +
+ + +
+ +
+
+ + +
+
+
Home +

Comfort under control

+

Loading devices…

+
+
+
+
--°C
+
+
+
+
+
Global power +

All air conditioners

+

Bulk power action for all units. OFF leaves thermostats locally off + until you explicitly turn a thermostat, group or the whole house back on.

+
+
+
+
+
+
+
+
+
Control plan +

What happens next

+
+
+
+
+
+ + + + +
+ +
+
+
LAN network +

Devices

+
+
+
+
+
+ +
+
+
Automation +

Zones

+
+
+
+

Zones define thermostat logic and sensor configuration. Use Quick + thermostats on the Dashboard for current temperature changes and presets.

+
+
+ +
+
+
Group control +

Groups

+
+
+

Combine thermostat zones and control only that set. Turning a group + on activates its thermostats; turning it off powers its units down and releases group ownership.

+
+
+ +
+
+
Visual logic +

Flow

+
+
+ +
+
One place for schedules and automations +

Build the logic from blocks. GREE Controller translates Flow into generated schedules and automations. Generated entries are read-only outside this editor.

+
+ Flow = source of truth +
+
+
+
+
+
+ +
+
+
Calendar +

Schedules

+
+
+ +
+
+

Quick schedule setup

+

Choose a zone and a ready-made daily rhythm. You + can edit every generated entry afterwards.

+
+ +
+ + + + + +
+
+
+
+ +
+
+
Rules +

Automations

+
+
+ +
Use thermostat presets for daily + comfort +

Schedules and zone presets handle normal heating/cooling automatically. + Keep automations for special exceptions so they do not fight the thermostat.

+
+
+
+ +
+
+
Simulator +

Automation simulator

+
+
+
+

Live preview of the thermostat and automation logic. The flow + board shows how sensor readings, schedules, night mode and thermostat decisions become commands sent to each + GREE unit.

+
+
+
+
+
Live flow +

Automation flow board

+
+
InputLogicAction
+
+
+
+
+
+
+
+
Timeline +

What will happen next

+
+
+
+
+
+
Rules +

Automation rules

+
+
+
+
+ +
+
+
Measurements +

Climate history

+
+
+
+ + + + + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
Comfort +

Night mode

+
+
+

Configure quieter thermostat behavior for sleeping hours without + mixing it with application settings.

+
+
+
+
+

Night mode

+

During selected hours the thermostat limits fan speed and can + request Quiet on supported units.

+
+
+
+ + + + + + +
+
+
Save night-mode changes.
+
+
+ +
+
+
Integration +

Home Assistant / Sensors

+
+
+

Connection, temperature sensors, friendly aliases and access + tokens for the Home Assistant integration.

+
+
+

Połączenie z Home Assistant

Adres serwera i dane dostępu używane przez wszystkie funkcje Home Assistant.

+
+ + + +

Use only for a trusted local Home Assistant server.

+
+
+
+ +
+

Źródła temperatury sterowania

Te ustawienia należą do logiki termostatów i źródeł temperatury, nie do wspólnych wejść Flow.

+
+ +

This entity is used by the Home Assistant connection test when no other entity is specified. It is not automatically used as a zone temperature source.

+ + +

If the HA sensor is not updated within this time, the reading is treated as stale.

+ +
+
+ +
+

Wspólne wejścia Flow

Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow. Czujniki HA dodane tylko tutaj nie są zapisywane do metryk.

+
+
+ +
+

Sensor aliases

Badges show whether an entity is collected as metrics, used by Flow, or both.

+
+
+
+
+
+ +
+

Home Assistant integration access

Create a controller token and paste it into the GREE Controller integration in Home Assistant.

+
+
+
Save Home Assistant and sensor changes.
+
+
+ +
+
+
System +

Settings

+
+
+
+ + +
+
+
+
+
+
+

Application runtime

+

Application-wide behavior that does not change GREE + transport or discovery.

+
+
+
+ +

When enabled, a prominent + warning is shown throughout the application.

+
+
+
+
+
+

Notifications

+

Send important events or only problems/anomalies.

+
+
+
+ + + +
+ + + + + +
+ Alert types +

Choose exactly which alerts may be + delivered. Thresholds above still define when applicable alerts are raised.

+
+ + + + + + + + +
+
+
+
+
+
+
+
+

Metrics and logs

+

SQLite keeps recent data locally and compacts older samples to + the resolution used by charts.

+
+
+
+ + + +
+
+
+
+
+

Long-term InfluxDB history

+

Optional archive for older history. InfluxDB 1.x and 2.x are + supported.

+
+
+
+ + + + +
+ + + +
+
+ + + +
+
+
+
+
+
+

On-screen debug

+

Live diagnostics displayed on every application page.

+
+
+
+ + +
+
+
+
+
+

Configuration backup

+

Export/import application configuration. + Exported files can contain GREE device keys plus Home Assistant and InfluxDB secrets; metrics and API + access tokens are not included.

+
+
+
+
+
+
+ +
Save changes made in the blocks + above.
+
+
+ +
+
+
Diagnostics +

Events

+
+
+
+
Log retentionOld event + rows are removed automatically during maintenance.
+
+
+
+
+ + + + + +
Blocks

Add block

+ +
+
+ + +

Actions

+ +
+ + + + +
+
Wspólne dane

Wspólne wejście Flow

+ + + +
+ +
+
+
+ + +
+
Flow library

Templates

+

Ready-made layouts create an editable graph using existing zones, devices and groups.

+
+
+
+
+
+
+ +
+
+
+ + +
+
Flow diagnostics

Dry-run

+
+

Dry-run does not change thermostat, device, group, schedule or automation state.

+ +
Sensor overrides

Leave blank to use the current application or Home Assistant state.

+
+
+
+
+
+ + + + +
+

More

+
+ +
+ + +
+
+

Add device

+
+ + + +
+ + +
+
+
+ + + +
+
+

Discover GREE devices

+
+

Auto searches both AES-ECB and AES-GCM devices. Multiple passes + improve discovery when several Wi-Fi modules answer the same broadcast.

+ +
+
+
+
+ + +
+
+

Name discovered devices

+
+

Give each new unit a friendly room name. The technical + model and MAC remain available in diagnostics.

+
+
+
+
+ + +
+ +
+

Name / protocol

+
+ + +

Changing protocol clears the saved device key and + performs a new bind on the next request.

+
+
+
+ + +
+ +
+

Zone

+
+ + +
+ +

If this zone should use the global Heating/Cooling mode + selected in GREE Controller, leave “Follow global mode” unchanged. Cooling only / Heating only forces a fixed + mode for this zone.

+
+ Comfort / sleep / away temperatures +
Cooling
+
Heating
+
+
+
+
+ +
+ +
+

The room sensor is assigned only to this zone. If it becomes + unavailable, the controller falls back to the GREE sensor.

+
+
+ + + +
+
+
+ + +
+
+
Compressor protection +

Command queue

+
+
+

Commands waiting for the compressor protection window to end. + You can cancel one task or clear the whole queue.

+
Waiting0
+
+
+
+
+ + +
+ +
+
Szybki termostat +

Włącz czasowo

+
+
+ + + + + + + + + + +
+ +
+
+ + + + + + +
+
+
+ + +
+ +
+

Group

+
+ +
+ Group members +
+
+

Groups are built from thermostat zones. ON takes control of + members; OFF powers their units down without leaving a group ownership lock.

+
+
+
+ + +
+ +
+

Schedule

+
+ + +
+ Weekdays +
+
+
+ + + +
+
+
+ + +
+ +
+

Automation

+
+
+ + + +
+
+

Action

+ + + +
+ +
+ + +
+
+
+ + +
+
+

Token created

+
+

Copy this token now. It will not be shown again.

+ +
+
+
+ + +
+
+
GREE ControllerAuthentication required
+
+ + +
+
+ + +
+ + + + \ No newline at end of file diff --git a/mock/mock.js b/mock/mock.js new file mode 100644 index 0000000..bba1a33 --- /dev/null +++ b/mock/mock.js @@ -0,0 +1,28 @@ +document.addEventListener('DOMContentLoaded',()=>{ + const q=new URLSearchParams(location.search); + document.documentElement.dataset.theme='dark'; + const view=q.get('view')||'dashboard'; + document.querySelectorAll('.view').forEach(v=>v.classList.toggle('active',v.dataset.view===view)); + if(view==='dashboard'){ + document.querySelector('#summaryText').textContent='3 urządzenia online · 2 aktywne termostaty'; + document.querySelector('#metrics').innerHTML='Online 3/3Aktywne 2'; + document.querySelector('#heroTemperature').innerHTML='21.4°C'; + document.querySelector('#houseClimate').innerHTML='

Tryb domu

Wspólne ustawienia dla stref.

Na zewnątrz 12.8°C
'; + document.querySelector('#controlPlan').innerHTML='

Salon

Utrzymuj 21.5°C do 22:30

Aktywne

Sypialnia

Tryb nocny od 22:30

za 2h
'; + } + if(view==='flows'){ + document.querySelector('#flowListCount').textContent='3 flow'; + document.querySelector('#flowList').innerHTML=`

Okno w salonie

Wyłącza grzanie, gdy okno jest otwarte.

Bloki5
Harmonogramy0
Automatyzacje1

Tani prąd

Podbija temperaturę przy niskiej cenie energii.

Bloki7
Harmonogramy1
Automatyzacje2
`; + } + if(q.get('editor')==='1'){ + document.querySelector('#flowEditor').hidden=false; document.body.classList.add('flow-editor-open'); + document.querySelector('#flowName').hidden=true; document.querySelector('#flowNameView').hidden=false; document.querySelector('#flowNameText').textContent='Okno w salonie'; + document.querySelector('#flowCompileStatus').textContent='0 harm. · 1 autom.'; document.querySelector('#flowSaveStatus').textContent='Zapisano'; + document.querySelector('#flowInterpretation').textContent='Jeśli okno w salonie jest otwarte przez 30 s, wyłącz termostat Salon.'; + document.querySelector('#flowRuntimeSummary').textContent='Ostatnie wykonanie: 07:12 · wynik: prawda'; + document.querySelector('#flowEmptyHint').style.display='none'; + document.querySelector('#flowNodes').innerHTML=`
Wspólne wejście
Okno w salonie
Terazotwarte
Utrzymuje się przez…
30 sekund
Termostat
Salon · wyłącz
`; + document.querySelector('#flowInspector').classList.add('has-selection'); + document.querySelector('#flowInspector').innerHTML='
Ustawienia bloku

Wspólne wejście

Czujnik używany tylko we Flow. Nie jest zapisywany do metryk.

'; + } +}); diff --git a/mock/styles.css b/mock/styles.css new file mode 100644 index 0000000..f4d6504 --- /dev/null +++ b/mock/styles.css @@ -0,0 +1,6763 @@ +:root { + color-scheme: dark; + --bg: #151515; + --canvas: #181818; + --surface: #1c1c1c; + --surface-2: #222222; + --surface-3: #282828; + --surface-muted: #202020; + --input-bg: #181818; + --nav-bg: #171717; + --dialog-bg: #1c1c1c; + --line: #2c2c2c; + --border: #2c2c2c; + --line-strong: #3a3a3a; + --text: #ededed; + --text-soft: #d4d4d4; + --muted: #a1a1aa; + --muted-2: #737373; + --muted-strong: #b5b5bd; + --accent: #3ecf8e; + --accent-strong: #46d99a; + --accent-soft: rgba(62, 207, 142, .10); + --accent-border: rgba(62, 207, 142, .36); + --accent-text: #06281a; + --danger: #f87171; + --danger-soft: rgba(248, 113, 113, .10); + --warning: #fbbf24; + --warning-soft: rgba(251, 191, 36, .10); + --info: #60a5fa; + --blue: #3b82f6; + --teal: #2dd4bf; + --purple: #a78bfa; + --orange: #fb923c; + --backdrop: rgba(0, 0, 0, .68); + --grid: rgba(255, 255, 255, .07); + --scroll-thumb: #444444; + --scroll-thumb-hover: #575757; + --radius: 16px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +:root[data-theme="light"] { + color-scheme: light; + --bg: #f8faf9; + --canvas: #f8faf9; + --surface: #ffffff; + --surface-2: #f5f6f6; + --surface-3: #eef0ef; + --surface-muted: #f6f7f7; + --input-bg: #ffffff; + --nav-bg: #ffffff; + --dialog-bg: #ffffff; + --line: #e5e7e6; + --border: #e5e7e6; + --line-strong: #d4d7d5; + --text: #1c1c1c; + --text-soft: #3f3f46; + --muted: #71717a; + --muted-2: #a1a1aa; + --muted-strong: #52525b; + --accent: #24b47e; + --accent-strong: #1ea672; + --accent-soft: rgba(36, 180, 126, .09); + --accent-border: rgba(36, 180, 126, .32); + --accent-text: #ffffff; + --danger: #dc5f5f; + --danger-soft: rgba(220, 95, 95, .08); + --warning: #b7791f; + --warning-soft: rgba(183, 121, 31, .08); + --info: #2563eb; + --blue: #2563eb; + --teal: #0f9f8f; + --purple: #7c3aed; + --orange: #c56b21; + --backdrop: rgba(23, 23, 23, .32); + --grid: rgba(24, 24, 27, .08); + --scroll-thumb: #c8ccca; + --scroll-thumb-hover: #aeb3b0; +} + +* { + box-sizing: border-box; +} + +html, +body { + background: var(--bg); +} + +body { + margin: 0; + min-height: 100vh; + color: var(--text); +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +button:disabled { + opacity: .5; + cursor: wait; +} + +.topbar { + position: sticky; + top: 0; + z-index: 20; + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + gap: 1rem; + min-height: 72px; + padding: 12px max(18px, env(safe-area-inset-left)); + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--nav-bg) 94%, transparent); + backdrop-filter: blur(14px); +} + +.brand { + display: flex; + align-items: center; + justify-self: start; + min-width: 0; +} + +.brand strong, +.brand small { + display: block; +} + +.brand strong { + font-size: 15px; + letter-spacing: -.01em; + white-space: nowrap; +} + +.brand small { + margin-top: 2px; + color: var(--muted); + font-size: 11px; +} + +.brand.large { + margin-bottom: 1rem; +} + +.brand.large strong { + font-size: 21px; +} + +.top-actions { + display: flex; + align-items: center; + justify-content: flex-end; + justify-self: end; + gap: 7px; +} + +.toolbar-runtime-status { + justify-self: center; + padding: 7px 10px; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--text-soft); + background: var(--surface-muted); + font-size: 11px; + font-weight: 750; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.toolbar-select { + width: auto; + min-width: 58px; + min-height: 40px; + padding: 7px 28px 7px 10px; + border-radius: 11px; +} + +.theme-select { + min-width: 94px; +} + +button { + border: 0; + border-radius: 13px; + padding: 11px 15px; + color: var(--text); + background: var(--surface-2); + transition: transform .15s ease, background .15s ease, opacity .15s ease; +} + +button:active { + transform: scale(.97); +} + +.primary { + background: var(--accent); + color: var(--accent-text); + font-weight: 800; +} + +.primary:hover { + background: var(--accent-strong); +} + +.secondary { + border: 1px solid var(--line); + background: var(--surface-muted); + font-weight: 650; +} + +.secondary:hover { + background: var(--surface-2); +} + +.danger { + color: var(--danger); +} + +.compact { + padding: 10px 14px; +} + +.icon-button { + width: 40px; + height: 40px; + padding: 0; + font-size: 22px; + background: transparent; +} + +main { + width: min(1180px, 100%); + margin: 0 auto; + padding: 24px 18px 108px; +} + +.view { + display: none; + animation: reveal .2s ease; +} + +.view.active { + display: block; +} + +@keyframes reveal { + from { + opacity: 0; + transform: translateY(5px); + } +} + +h1, +h2, +h3, +p { + margin-top: 0; +} + +h1 { + margin-bottom: 8px; + font-size: clamp(28px, 6vw, 44px); + letter-spacing: -.045em; +} + +h2 { + margin-bottom: 0; + font-size: clamp(23px, 4vw, 31px); + letter-spacing: -.035em; +} + +h3 { + margin-bottom: 10px; +} + +.eyebrow { + color: var(--accent); + font-size: 11px; + font-weight: 850; + letter-spacing: .14em; + text-transform: uppercase; +} + +.lead { + max-width: 690px; + color: var(--muted); + line-height: 1.55; +} + +.hero { + position: relative; + overflow: hidden; + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 18px; + min-height: 210px; + padding: clamp(24px, 6vw, 44px); + border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line)); + border-radius: 30px; + background: var(--surface); +} + +.hero::after { + content: ""; + position: absolute; + right: -80px; + bottom: -120px; + width: 220px; + height: 220px; + border: 34px solid color-mix(in srgb, var(--accent) 7%, transparent); + border-radius: 50%; +} + +.hero h1 { + margin: 5px 0 8px; +} + +.hero p { + margin: 0; + color: var(--muted); +} + +.hero-temp { + z-index: 1; + white-space: nowrap; + font-size: clamp(48px, 12vw, 88px); + font-weight: 250; + letter-spacing: -.075em; +} + +.hero-temp small { + margin-left: 4px; + color: var(--muted); + font-size: .28em; + font-weight: 600; + letter-spacing: 0; +} + +.metrics { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin: 14px 0 34px; +} + +.metric { + padding: 18px; + border: 1px solid var(--line); + border-radius: 18px; + background: var(--surface); +} + +.metric span { + display: block; + margin-bottom: 8px; + color: var(--muted); + font-size: 12px; +} + +.metric strong { + font-size: 24px; + letter-spacing: -.03em; +} + +.section-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 16px; + margin: 8px 0 18px; +} + +.section-heading-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.device-grid, +.list-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); + gap: 14px; +} + +.device-card, +.panel, +.list-card { + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface); +} + +.device-card { + overflow: hidden; + padding: 20px; +} + +.device-card.off { + opacity: .86; +} + +.device-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.device-title { + min-width: 0; +} + +.device-title h3 { + overflow: hidden; + margin: 0 0 4px; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 19px; +} + +.device-title p { + overflow: hidden; + margin: 0; + color: var(--muted); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--muted); +} + +.status::before { + content: ""; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--danger); +} + +.status.online::before { + background: var(--accent); +} + +.power-button { + display: grid; + place-items: center; + flex: 0 0 auto; + width: 46px; + height: 46px; + padding: 0; + border-radius: 50%; + color: var(--muted); + font-size: 21px; +} + +.power-button.on { + background: var(--accent); + color: var(--accent-text); +} + +.temperature-control { + display: flex; + align-items: center; + justify-content: center; + gap: 22px; + padding: 24px 0 17px; +} + +.temperature-control button { + width: 42px; + height: 42px; + padding: 0; + border: 1px solid var(--line); + border-radius: 50%; + background: transparent; + font-size: 22px; +} + +.target-temp { + min-width: 106px; + text-align: center; + font-size: 48px; + font-weight: 300; + letter-spacing: -.06em; +} + +.target-temp small { + margin-left: 3px; + color: var(--muted); + font-size: 15px; + font-weight: 650; +} + +.editable-target[data-editable="true"], +.editable-target:not([data-editable]) { + cursor: text; + border-radius: 10px; + transition: background .15s ease, box-shadow .15s ease; +} + +.editable-target[data-editable="true"]:hover, +.editable-target[data-editable="true"]:focus-visible, +.editable-target:not([data-editable]):hover, +.editable-target:not([data-editable]):focus-visible { + background: var(--surface-2); + box-shadow: 0 0 0 1px var(--line); + outline: none; +} + +.editable-target.editing { + min-width: 92px; +} + +.inline-temperature-input { + width: 92px; + max-width: 100%; + padding: 3px 6px; + border: 1px solid var(--line); + border-radius: 9px; + background: var(--surface); + color: var(--text); + font: inherit; + font-size: .72em; + font-weight: 500; + text-align: center; + letter-spacing: 0; + outline: none; +} + +.inline-temperature-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent); +} + +.hysteresis-config { + display: grid; + gap: 10px; +} + +.hysteresis-config .check { + margin-top: 0; +} + +.current-line { + display: flex; + justify-content: center; + gap: 6px; + margin: -10px 0 18px; + color: var(--muted); + font-size: 12px; +} + +.current-line strong { + color: var(--text); +} + +.mode-row, +.fan-row { + display: flex; + gap: 7px; + overflow-x: auto; + padding: 2px 0 10px; + scrollbar-width: none; +} + +.mode-row::-webkit-scrollbar, +.fan-row::-webkit-scrollbar { + display: none; +} + +.mode-row button, +.fan-row button { + flex: 1 0 auto; + min-width: 58px; + padding: 9px 10px; + color: var(--muted); + background: var(--surface-muted); + font-size: 12px; +} + +.mode-row button.active, +.fan-row button.active, +.house-mode-row button.active { + color: var(--accent-text); + background: var(--accent); + font-weight: 800; +} + +.device-toggles { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 7px; + margin-top: 4px; +} + +.device-toggles button { + padding: 9px 5px; + color: var(--muted); + font-size: 11px; +} + +.device-toggles button.active { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, var(--surface)); +} + +.card-footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid var(--line); +} + +.card-footer small { + overflow: hidden; + color: var(--muted); + text-overflow: ellipsis; + white-space: nowrap; +} + +.device-network-info { + display: grid; + min-width: 0; + gap: 2px; +} + +.device-network-error { + color: var(--danger) !important; +} + +.card-menu { + display: flex; + gap: 5px; +} + +.card-menu button { + padding: 7px 9px; + background: transparent; + color: var(--muted); + font-size: 12px; +} + +.list-card { + padding: 19px; +} + +.list-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.list-card h3 { + margin: 0 0 5px; + font-size: 18px; +} + +.list-card p { + margin: 0; + color: var(--muted); + font-size: 13px; + line-height: 1.45; +} + +.badge { + display: inline-flex; + align-items: center; + padding: 5px 9px; + border-radius: 99px; + color: var(--muted); + background: var(--surface-muted); + font-size: 11px; +} + +.badge.active { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, var(--surface)); +} + +.badge.manual-override { + color: var(--warning); + background: var(--warning-soft); +} + +.list-card.manual-takeover { + border-color: var(--warning); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning) 24%, transparent); +} + +.manual-override-panel { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 8px; + padding: 11px 12px; + border: 1px solid color-mix(in srgb, var(--warning) 45%, var(--line)); + border-radius: 12px; + background: var(--warning-soft); +} + +.manual-override-panel>div { + flex: 1 1 220px; + min-width: 0; +} + +.manual-override-panel strong { + display: block; + margin-bottom: 4px; + color: var(--warning); + font-size: 13px; +} + +.manual-override-panel p { + margin: 0; + color: var(--text); + font-size: 12px; + line-height: 1.45; +} + +.manual-override-panel button { + flex: 0 0 auto; + min-height: 34px; + padding: 7px 11px; +} + +.zone-enable-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 30px; + padding: 5px 9px; + border: 1px solid var(--line); + border-radius: 99px; + color: var(--muted); + background: var(--surface-muted); + font-size: 11px; +} + +.zone-enable-toggle.active { + border-color: color-mix(in srgb, var(--accent) 34%, var(--line)); + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, var(--surface)); +} + +.zone-enable-toggle span { + font-size: 12px; + font-weight: 900; +} + +.zone-thermostat.zone-disabled .thermostat-main, +.zone-thermostat.zone-disabled .preset-row, +.zone-thermostat.zone-disabled .zone-mode-row, +.zone-thermostat.zone-disabled .zone-state-line { + opacity: .5; +} + +.card-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 7px; + margin-top: 16px; +} + +.card-stat { + padding: 10px; + border-radius: 12px; + background: var(--surface-muted); + text-align: center; +} + +.card-stat small, +.card-stat strong { + display: block; +} + +.card-stat small { + margin-bottom: 5px; + color: var(--muted); + font-size: 10px; +} + +.card-stat strong { + font-size: 15px; +} + +.empty { + grid-column: 1/-1; + padding: 38px 24px; + border: 1px dashed var(--line); + border-radius: var(--radius); + color: var(--muted); + text-align: center; +} + +.empty strong { + display: block; + margin-bottom: 7px; + color: var(--text); +} + +.panel { + padding: 20px; +} + +.chart-panel { + overflow: hidden; +} + +.chart-toolbar { + display: flex; + flex-wrap: wrap; + gap: 9px; + margin-bottom: 18px; +} + +.chart-toolbar select { + flex: 1; + min-width: 140px; +} + +.chart-wrap { + position: relative; + overflow-x: auto; + min-height: 310px; +} + +#historyChart { + width: 100%; + min-width: 680px; + height: 340px; +} + +.legend { + display: flex; + gap: 18px; + margin-top: 10px; + color: var(--muted); + font-size: 12px; +} + +.legend>span { + display: flex; + align-items: center; + gap: 7px; +} + +.dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--accent); +} + +.dot.target { + background: var(--warning); +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px; +} + +.form-grid h3, +.form-grid hr, +.form-grid .wide { + grid-column: 1/-1; +} + +.form-grid hr, +.dialog-form hr { + width: 100%; + border: 0; + border-top: 1px solid var(--line); +} + +label { + display: grid; + gap: 7px; + color: var(--muted); + font-size: 12px; + font-weight: 650; +} + +input, +select { + width: 100%; + min-height: 45px; + border: 1px solid var(--line); + border-radius: 12px; + outline: 0; + padding: 10px 12px; + color: var(--text); + background: var(--input-bg); +} + +input:focus, +select:focus { + border-color: var(--accent); + outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent); + outline-offset: 1px; +} + +.check { + display: flex; + grid-auto-flow: column; + justify-content: start; + align-items: center; + gap: 9px; + min-height: 44px; +} + +.check input { + width: 19px; + min-height: 19px; + accent-color: var(--accent); +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 9px; + margin-top: 6px; +} + +.influx-fields { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px; + padding: 14px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--surface-muted); +} + +.influx-fields[hidden] { + display: none; +} + +.influx-fields label:last-child:nth-child(odd) { + grid-column: 1/-1; +} + +.backup-actions { + align-items: center; +} + +.system-panel { + display: grid; + gap: 16px; + margin-top: 14px; + padding: 18px; +} + +.system-panel-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.system-panel-head h3 { + margin: 4px 0 5px; + font-size: 20px; +} + +.system-panel-head p { + max-width: 700px; + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.5; +} + +.system-health { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 6px 10px; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--muted); + background: var(--surface-muted); + font-size: 10px; + font-weight: 800; + white-space: nowrap; +} + +.system-health.ok { + border-color: var(--accent-border); + color: var(--accent); + background: var(--accent-soft); +} + +.system-health.warn { + border-color: color-mix(in srgb, var(--warning) 35%, var(--line)); + color: var(--warning); + background: var(--warning-soft); +} + +.system-status-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); + gap: 9px; +} + +.system-status-item { + min-width: 0; + padding: 12px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--surface-muted); +} + +.system-status-item small, +.system-status-item strong { + display: block; +} + +.system-status-item small { + margin-bottom: 5px; + color: var(--muted); + font-size: 10px; +} + +.system-status-item strong { + overflow: hidden; + color: var(--text); + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.system-status-item.ok { + border-color: color-mix(in srgb, var(--accent) 25%, var(--line)); +} + +.system-status-item.warn { + border-color: color-mix(in srgb, var(--warning) 28%, var(--line)); + background: color-mix(in srgb, var(--warning-soft) 52%, var(--surface-muted)); +} + +.system-status-item.mono strong { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 11px; +} + +.gree-frame-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 10px; +} + +.gree-frame-stat { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; + padding: 12px 14px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--surface-2); +} + +.gree-frame-stat small { + overflow: hidden; + color: var(--muted); + text-overflow: ellipsis; + white-space: nowrap; +} + +.gree-frame-stat strong { + color: var(--text); + font-variant-numeric: tabular-nums; +} + +.gree-frame-stat.total { + border-color: var(--accent-border); + background: var(--accent-soft); +} + +.quick-thermostat-control .zone-mode-row button:disabled { + opacity: .38; + cursor: not-allowed; + box-shadow: none; +} + +.log-list { + display: grid; + gap: 2px; + padding: 8px; +} + +.log-row { + display: grid; + grid-template-columns: 78px 150px 1fr; + gap: 12px; + padding: 11px 12px; + border-bottom: 1px solid var(--line); + font-size: 12px; +} + +.log-row:last-child { + border-bottom: 0; +} + +.log-row time, +.log-row .kind { + color: var(--muted); +} + +.log-row.error .kind { + color: var(--danger); +} + +.log-row.warn .kind { + color: var(--warning); +} + +.bottom-nav { + position: fixed; + z-index: 30; + right: 0; + bottom: 0; + left: 0; + display: grid; + grid-template-columns: repeat(5, 1fr); + padding: 8px max(8px, env(safe-area-inset-right)) calc(8px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); + border-top: 1px solid var(--line); + background: color-mix(in srgb, var(--nav-bg) 96%, transparent); + backdrop-filter: blur(14px); +} + +.bottom-nav button { + display: grid; + place-items: center; + gap: 3px; + padding: 7px 2px; + border-radius: 12px; + color: var(--muted); + background: transparent; + font-size: 10px; +} + +.bottom-nav button span { + font-size: 20px; + line-height: 1; +} + +.bottom-nav button b { + font-weight: 650; +} + +.bottom-nav button.active { + color: var(--accent); +} + +dialog { + width: min(540px, calc(100% - 24px)); + max-height: min(88vh, 760px); + overflow: auto; + border: 1px solid var(--line); + border-radius: 24px; + padding: 0; + color: var(--text); + background: var(--dialog-bg); +} + +dialog::backdrop { + background: var(--backdrop); + backdrop-filter: blur(4px); +} + +.dialog-wide { + width: min(860px, calc(100% - 24px)); +} + +.dialog-form { + display: grid; + gap: 14px; + padding: 20px; +} + +.dialog-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.dialog-head h2 { + font-size: 23px; +} + +.dialog-head button { + width: 38px; + height: 38px; + padding: 0; + background: transparent; + color: var(--muted); + font-size: 25px; +} + +.two { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +#externalSensorFields { + display: grid; + gap: 12px; +} + +.field-note { + margin: -4px 0 0; + color: var(--muted); + font-size: .8rem; + line-height: 1.45; +} + +.sensor-detail { + margin-top: 10px; + color: var(--muted); + font-size: .8rem; + line-height: 1.45; +} + +fieldset { + border: 1px solid var(--line); + border-radius: 14px; + padding: 10px; +} + +legend { + padding: 0 5px; + color: var(--muted); + font-size: 11px; +} + +.days { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 4px; +} + +.days label { + display: grid; + place-items: center; + gap: 5px; + padding: 6px 2px; + border-radius: 8px; + background: var(--surface-muted); + font-size: 10px; +} + +.days input { + width: 16px; + min-height: 16px; + accent-color: var(--accent); +} + +.sheet { + width: min(500px, calc(100% - 18px)); + margin: auto auto 10px; +} + +.sheet .dialog-head { + padding: 18px 18px 5px; +} + +.menu-list { + display: grid; + padding: 8px 10px 12px; +} + +.menu-list button { + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--line); + border-radius: 0; + padding: 16px 9px; + background: transparent; + text-align: left; +} + +.menu-list button:last-child { + border-bottom: 0; +} + +.auth-dialog { + width: min(420px, calc(100% - 30px)); +} + +@media (min-width: 900px) { + .bottom-nav { + top: calc(var(--topbar-height, 72px) + 20px); + right: auto; + bottom: auto; + left: max(16px, calc((100vw - 1380px)/2)); + grid-template-columns: 1fr; + width: 90px; + border: 1px solid var(--line); + border-radius: 22px; + padding: 9px; + background: var(--surface); + } + + .bottom-nav button { + padding: 10px 4px; + } + + main { + padding-left: 122px; + padding-bottom: 50px; + } +} + +@media (max-width: 720px) { + .topbar { + flex-wrap: wrap; + } + + .top-actions { + width: 100%; + } + + .top-actions .primary { + margin-left: auto; + } +} + +@media (max-width: 620px) { + main { + padding: 18px 13px 105px; + } + + .hero { + min-height: 190px; + padding: 23px; + } + + .hero-temp { + font-size: 52px; + } + + .metrics { + gap: 7px; + } + + .metric { + padding: 14px 10px; + } + + .metric strong { + font-size: 20px; + } + + .section-heading { + align-items: center; + } + + .section-heading button { + padding: 9px 11px; + font-size: 12px; + } + + .form-grid { + grid-template-columns: 1fr; + } + + .form-grid h3, + .form-grid hr, + .form-grid .wide { + grid-column: auto; + } + + .log-row { + grid-template-columns: 62px 1fr; + } + + .log-row .message { + grid-column: 1/-1; + } + + .theme-select { + min-width: 86px; + } +} + +@media (max-width: 410px) { + .topbar { + padding-inline: 12px; + } + + .brand strong { + font-size: 13px; + } + + .hero { + align-items: start; + flex-direction: column; + } + + .hero-temp { + align-self: flex-end; + margin-top: -15px; + } + + .two { + grid-template-columns: 1fr; + } + + .toolbar-select { + min-width: 54px; + } + + .theme-select { + min-width: 82px; + } +} + +.token-manager { + display: grid; + gap: 12px; +} + +.token-list { + display: grid; + gap: 8px; +} + +.token-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--surface-muted); +} + +.token-row>div { + min-width: 0; + display: grid; + gap: 3px; +} + +.token-row strong { + color: var(--text); + font-size: 13px; +} + +.token-row small { + overflow: hidden; + color: var(--muted); + text-overflow: ellipsis; + white-space: nowrap; +} + +.mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +.empty.compact { + padding: 18px; +} + +@media (max-width: 620px) { + .token-row { + align-items: stretch; + flex-direction: column; + } + + .token-row button { + width: 100%; + } +} + +/* Zone quick controls */ +.zone-quick-control { + display: grid; + gap: 4px; + margin-top: 10px; + padding: 8px 10px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--surface-muted); +} + +.temperature-control.compact { + padding: 4px 0 7px; + gap: 16px; +} + +.temperature-control.compact button { + width: 38px; + height: 38px; +} + +.target-temp.compact { + min-width: 92px; + font-size: 32px; +} + +.zone-quick-control .mode-row { + padding-bottom: 2px; +} + +.discovery-name-list { + display: grid; + gap: 12px; +} + +.discovery-name-row { + display: grid; + gap: 8px; + padding: 12px 0; + border-bottom: 1px solid var(--border); +} + +.discovery-name-row:last-child { + border-bottom: 0; +} + +.discovery-name-row span { + display: grid; + gap: 2px; +} + +.discovery-name-row small { + color: var(--muted); + overflow-wrap: anywhere; +} + +/* Themed scrollbars: visible where scrolling is useful, but visually quiet. */ +* { + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--accent) 42%, var(--surface-2)) var(--surface-muted); +} + +*::-webkit-scrollbar { + width: 9px; + height: 9px; +} + +*::-webkit-scrollbar-track { + background: var(--surface-muted); + border-radius: 99px; +} + +*::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--accent) 38%, var(--surface-2)); + border: 2px solid var(--surface-muted); + border-radius: 99px; +} + +*::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--accent) 58%, var(--surface-2)); +} + +.mode-row, +.fan-row, +.preset-row { + scrollbar-width: none; +} + +.mode-row::-webkit-scrollbar, +.fan-row::-webkit-scrollbar, +.preset-row::-webkit-scrollbar { + display: none; +} + +.house-climate { + display: grid; + gap: 16px; + margin: 0 0 12px; +} + +.house-climate-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.house-climate-head h3 { + margin: 2px 0 4px; + font-size: 20px; +} + +.house-climate-head p { + margin: 0; + color: var(--muted); + font-size: 13px; + line-height: 1.45; +} + +.outside-pill { + flex: 0 0 auto; + min-width: 94px; + padding: 10px 13px; + border-radius: 14px; + background: var(--surface-muted); + text-align: right; +} + +.outside-pill small, +.outside-pill strong { + display: block; +} + +.outside-pill small { + color: var(--muted); + font-size: 10px; +} + +.outside-pill strong { + margin-top: 3px; + font-size: 18px; +} + +.automation-plan-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr)); + gap: 12px; + margin-bottom: 34px; +} + +.plan-card { + display: grid; + align-content: start; + gap: 11px; + min-height: 190px; + padding: 17px; +} + +.plan-card.disabled { + opacity: .65; +} + +.plan-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.plan-card-head h3 { + margin: 2px 0 0; + font-size: 19px; +} + +.plan-card>p { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.plan-temp { + display: flex; + align-items: baseline; + gap: 9px; +} + +.plan-temp>span { + color: var(--muted); + font-size: 20px; +} + +.plan-temp>b { + color: var(--muted); + font-weight: 500; +} + +.plan-temp>strong { + font-size: 28px; + letter-spacing: -.04em; +} + +.plan-temp small { + margin-left: 2px; + color: var(--muted); + font-size: 11px; +} + +.plan-events { + display: grid; + gap: 6px; + margin: 0; + padding: 10px 0 0; + border-top: 1px solid var(--line); + list-style: none; +} + +.plan-events li { + display: grid; + grid-template-columns: 82px 1fr; + gap: 8px; + color: var(--text-soft); + font-size: 11px; + line-height: 1.35; +} + +.plan-events time { + color: var(--muted); + font-variant-numeric: tabular-nums; +} + +.plan-events .muted { + display: block; + color: var(--muted); +} + +.plan-house { + border-color: color-mix(in srgb, var(--accent) 30%, var(--line)); +} + +.plan-loading { + grid-column: 1/-1; + padding: 18px; + color: var(--muted); +} + +.house-mode-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; +} + +.house-mode-row button { + min-height: 44px; + color: var(--muted); + background: var(--surface-muted); +} + +.house-power-panel { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + margin: -15px 0 12px; +} + +.house-power-panel h3 { + margin: 2px 0 4px; + font-size: 18px; +} + +.house-power-panel p { + margin: 0; + max-width: 650px; + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.house-power-actions { + display: flex; + flex: 0 0 auto; + gap: 8px; +} + +.house-power-actions button { + min-height: 40px; +} + +.house-power-actions button.active { + color: var(--accent-text); + background: var(--accent); + font-weight: 800; +} + +.zone-thermostat { + overflow: hidden; +} + +.zone-thermostat.demanding { + border-color: color-mix(in srgb, var(--accent) 38%, var(--line)); +} + +.thermostat-main { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 8px; + margin: 15px 0 9px; +} + +.thermostat-main>div:first-child { + text-align: left; +} + +.thermostat-main>div:last-child { + text-align: right; +} + +.thermostat-main small, +.thermostat-main strong { + display: block; +} + +.thermostat-main small { + margin-bottom: 3px; + color: var(--muted); + font-size: 10px; +} + +.thermostat-main strong { + font-size: 16px; +} + +.preset-row { + display: flex; + gap: 6px; + overflow-x: auto; + padding: 3px 0 8px; +} + +.preset-row button { + flex: 1 0 auto; + min-height: 38px; + padding: 8px 11px; + color: var(--muted); + background: var(--surface-muted); + font-size: 12px; +} + +.preset-row button.active { + color: var(--accent-text); + background: var(--accent); + font-weight: 800; +} + +.zone-mode-row { + padding-bottom: 5px; +} + +.zone-state-line { + display: flex; + justify-content: space-between; + gap: 10px; + padding: 8px 1px 1px; + color: var(--muted); + font-size: 11px; +} + +.zone-state-line span:last-child { + text-align: right; +} + +.dashboard-zones { + margin-bottom: 34px; +} + +.schedule-template-panel { + display: grid; + gap: 13px; + margin-bottom: 18px; +} + +.schedule-template-panel h3 { + margin-bottom: 4px; +} + +.template-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 8px; +} + +.profile-editor { + border: 1px solid var(--line); + border-radius: 14px; + padding: 11px 12px; + background: var(--surface-muted); +} + +.profile-editor summary { + cursor: pointer; + color: var(--text); + font-weight: 700; + font-size: 13px; +} + +.profile-editor[open] summary { + margin-bottom: 12px; +} + +.profile-grid { + display: grid; + grid-template-columns: 90px repeat(3, 1fr); + align-items: end; + gap: 8px; + margin-top: 8px; +} + +.profile-grid>strong { + align-self: center; + font-size: 12px; +} + +.profile-grid label span { + font-size: 10px; +} + +.warning-note { + color: var(--warning); +} + +@media (max-width: 620px) { + .house-power-panel { + margin-top: -20px; + } + + .house-climate-head { + align-items: flex-start; + } + + .house-climate-head p { + max-width: 220px; + } + + .house-power-panel { + align-items: stretch; + flex-direction: column; + } + + .house-power-actions { + display: grid; + grid-template-columns: 1fr 1fr; + } + + .thermostat-main { + grid-template-columns: 1fr auto 1fr; + gap: 4px; + } + + .thermostat-main .temperature-control.compact { + gap: 9px; + } + + .thermostat-main .target-temp.compact { + min-width: 75px; + font-size: 28px; + } + + .profile-grid { + grid-template-columns: 1fr 1fr; + } + + .profile-grid>strong { + grid-column: 1/-1; + margin-top: 4px; + } + + .zone-state-line { + flex-direction: column; + gap: 2px; + } + + .zone-state-line span:last-child { + text-align: left; + } +} + +.automation-hint { + margin-bottom: 18px; +} + +.automation-hint strong { + display: block; + margin-bottom: 5px; +} + +.automation-hint p { + margin: 0; + color: var(--muted); + font-size: 13px; + line-height: 1.5; +} + +/* Rich climate history (v0.4.4) using the classic rounded UI. */ +.history-chart-card { + margin-top: 14px; +} + +.chart-title-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.chart-title-row h3 { + margin: 0 0 4px; + font-size: 17px; +} + +.chart-title-row p { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.history-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 9px; +} + +.history-stat { + min-width: 0; + padding: 12px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--surface-muted); +} + +.history-stat small, +.history-stat strong, +.history-stat span { + display: block; +} + +.history-stat small { + overflow: hidden; + color: var(--muted); + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.history-stat strong { + margin-top: 4px; + color: var(--text); + font-size: 18px; + font-weight: 700; +} + +.history-stat span { + overflow: hidden; + margin-top: 2px; + color: var(--muted-2); + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chart-wrap { + scrollbar-width: none; + -ms-overflow-style: none; +} + +.chart-wrap::-webkit-scrollbar { + display: none; +} + +.chart-wrap.compact-chart { + min-height: 230px; +} + +#historyTemperatureChart, +#historyOperationChart { + width: 100%; + min-width: 720px; +} + +#historyTemperatureChart { + height: 360px; +} + +#historyOperationChart { + height: 260px; +} + +.legend { + flex-wrap: wrap; + gap: 7px 10px; +} + +.legend-item { + display: inline-flex; + align-items: center; + gap: 7px; + min-height: 30px; + padding: 5px 8px; + border: 1px solid transparent; + border-radius: 9px; + background: transparent; + color: var(--muted); + font: inherit; + cursor: pointer; +} + +.legend-item:hover { + border-color: var(--line); + background: var(--surface-muted); + color: var(--text-soft); +} + +.legend-item.is-hidden { + opacity: .42; + text-decoration: line-through; +} + +.legend-item:focus-visible { + outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); + outline-offset: 1px; +} + +.legend-line { + flex: 0 0 18px; + width: 18px; + height: 3px; + border-radius: 999px; + background: var(--legend-color, var(--accent)); +} + +@media (max-width: 620px) { + .history-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .chart-toolbar { + display: grid; + grid-template-columns: 1fr 1fr; + } + + .chart-toolbar button { + grid-column: 1 / -1; + } +} + +/* History navigation, custom chart composer and linkable sub-pages. */ +.history-tabs { + display: flex; + gap: 7px; + overflow-x: auto; + margin: 0 0 14px; + padding: 3px; + scrollbar-width: none; +} + +.history-tabs::-webkit-scrollbar { + display: none; +} + +.history-tabs button { + flex: 0 0 auto; + padding: 9px 13px; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--muted); + background: var(--surface); + font-size: 12px; + font-weight: 700; +} + +.history-tabs button.active { + color: var(--accent-text); + border-color: var(--accent); + background: var(--accent); +} + +.history-toolbar-panel { + margin-bottom: 14px; +} + +.history-context-controls { + display: flex; + flex: 1 1 260px; + min-width: min(100%, 220px); +} + +.history-context-controls label { + width: 100%; +} + +.history-context-hint { + align-self: center; + color: var(--muted); + font-size: 12px; + line-height: 1.4; +} + +.history-charts { + display: grid; + gap: 14px; +} + +.history-custom-builder { + display: none; + margin-bottom: 14px; +} + +.history-custom-builder.active { + display: block; +} + +.custom-chart-panel { + display: grid; + gap: 13px; +} + +.custom-chart-add, +.custom-chart-save { + display: flex; + gap: 8px; + align-items: center; +} + +.custom-chart-add select, +.custom-chart-save input { + flex: 1; +} + +.custom-series-list { + display: flex; + flex-wrap: wrap; + gap: 7px; + min-height: 34px; + align-items: center; +} + +.custom-series-chip { + display: inline-flex; + align-items: center; + gap: 7px; + max-width: 100%; + padding: 7px 8px 7px 10px; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--surface-muted); + color: var(--text-soft); + font-size: 11px; +} + +.custom-series-chip i { + width: 8px; + height: 8px; + flex: 0 0 auto; + border-radius: 50%; + background: var(--chip-color, var(--accent)); +} + +.custom-series-chip button { + width: 23px; + height: 23px; + padding: 0; + border-radius: 50%; + color: var(--muted); + background: transparent; + font-size: 16px; + line-height: 1; +} + +.saved-chart-list { + display: grid; + gap: 7px; + padding-top: 3px; + border-top: 1px solid var(--line); +} + +.saved-chart-row { + display: grid; + grid-template-columns: 1fr auto; + gap: 7px; +} + +.saved-chart-row>button:first-child { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--line); + background: var(--surface-muted); + text-align: left; +} + +.saved-chart-row strong, +.saved-chart-row small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.saved-chart-row small { + color: var(--muted); +} + +.saved-chart-row>.danger { + width: 42px; + padding: 0; + border: 1px solid var(--line); + background: var(--surface-muted); +} + +/* Toast stack: quiet success/error feedback that matches light and dark themes. */ +.toast-stack { + position: fixed; + z-index: 120; + right: 18px; + bottom: 92px; + display: grid; + gap: 9px; + width: min(390px, calc(100vw - 36px)); + pointer-events: none; +} + +.toast-item { + position: relative; + display: grid; + grid-template-columns: 34px 1fr 28px; + gap: 10px; + align-items: center; + overflow: hidden; + padding: 12px 11px; + border: 1px solid var(--line); + border-radius: 16px; + background: color-mix(in srgb, var(--surface) 96%, transparent); + backdrop-filter: blur(16px); + opacity: 0; + transform: translateY(10px) scale(.98); + transition: opacity .2s ease, transform .2s ease; + pointer-events: auto; +} + +.toast-item.show { + opacity: 1; + transform: translateY(0) scale(1); +} + +.toast-item.leaving { + opacity: 0; + transform: translateX(18px) scale(.98); +} + +.toast-item.success { + border-color: color-mix(in srgb, var(--accent) 34%, var(--line)); +} + +.toast-item.error { + border-color: color-mix(in srgb, var(--danger) 42%, var(--line)); +} + +.toast-icon { + display: grid; + place-items: center; + width: 32px; + height: 32px; + border-radius: 50%; + color: var(--accent); + background: var(--accent-soft); + font-size: 15px; + font-weight: 900; +} + +.toast-item.error .toast-icon { + color: var(--danger); + background: var(--danger-soft); +} + +.toast-copy { + min-width: 0; +} + +.toast-copy strong, +.toast-copy span { + display: block; +} + +.toast-copy strong { + margin-bottom: 2px; + font-size: 12px; +} + +.toast-copy span { + overflow-wrap: anywhere; + color: var(--muted); + font-size: 11px; + line-height: 1.35; +} + +.toast-close { + width: 28px; + height: 28px; + padding: 0; + border-radius: 50%; + color: var(--muted); + background: transparent; + font-size: 19px; +} + +.toast-progress { + position: absolute; + right: 11px; + bottom: 3px; + left: 11px; + height: 2px; + border-radius: 99px; + background: var(--accent); + transform-origin: left; + animation: toast-progress 3.6s linear forwards; +} + +.toast-item.error .toast-progress { + background: var(--danger); + animation-duration: 5.2s; +} + +@keyframes toast-progress { + from { + transform: scaleX(1); + } + + to { + transform: scaleX(0); + } +} + +.debug-overlay { + position: fixed; + z-index: 110; + right: 18px; + bottom: 92px; + width: min(680px, calc(100vw - 36px)); + max-height: min(46vh, 440px); + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line)); + border-radius: 18px; + background: color-mix(in srgb, var(--surface) 96%, transparent); + box-shadow: 0 18px 60px rgba(0, 0, 0, .22); + backdrop-filter: blur(18px); +} + +.debug-overlay[hidden] { + display: none; +} + +.debug-overlay-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-bottom: 1px solid var(--line); +} + +.debug-overlay-title { + display: grid; + flex: 0 1 auto; + gap: 2px; + min-width: 90px; +} + +.debug-overlay-head strong { + font-size: 12px; +} + +.debug-overlay-head small { + color: var(--muted); + font-size: 10px; +} + +.debug-overlay-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 7px; + min-width: 0; +} + +.debug-source-tabs { + display: flex; + align-items: center; + gap: 3px; + padding: 3px; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--surface-muted); +} + +.debug-source-tabs button, +.debug-overlay-head .debug-clear { + min-height: 28px; + padding: 5px 8px; + border-radius: 7px; + color: var(--muted); + background: transparent; + font-size: 9px; + font-weight: 750; +} + +.debug-source-tabs button.active { + color: var(--accent); + background: var(--accent-soft); +} + +.debug-overlay-head .debug-clear { + border: 1px solid var(--line); + background: var(--surface-muted); +} + +.debug-overlay-lines { + max-height: min(38vh, 360px); + overflow: auto; + padding: 7px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +.debug-line { + display: grid; + grid-template-columns: 70px 48px 1fr; + gap: 8px; + margin: 3px 0; + padding: 7px 8px; + border: 1px solid var(--line); + border-radius: 9px; + background: color-mix(in srgb, var(--surface-muted) 76%, transparent); + font-size: 10px; + line-height: 1.35; +} + +.debug-line.source-gree { + border-color: color-mix(in srgb, var(--accent) 25%, var(--line)); +} + +.debug-line.source-request { + border-color: color-mix(in srgb, var(--info) 24%, var(--line)); +} + +.debug-line time, +.debug-line b { + color: var(--muted); + font-weight: 650; +} + +.debug-line.source-gree b { + color: var(--accent); +} + +.debug-line.source-request b { + color: var(--info); +} + +.debug-line span { + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.debug-empty { + padding: 18px; + color: var(--muted); + text-align: center; + font-size: 11px; +} + +@media (max-width: 620px) { + + .custom-chart-add, + .custom-chart-save { + align-items: stretch; + flex-direction: column; + } + + .toast-stack { + right: 12px; + bottom: 88px; + width: calc(100vw - 24px); + } + + .debug-overlay { + right: 12px; + bottom: 88px; + width: calc(100vw - 24px); + } + + .debug-overlay-head { + align-items: stretch; + flex-direction: column; + } + + .debug-overlay-actions { + justify-content: space-between; + } + + .debug-source-tabs { + flex: 1 1 auto; + } + + .debug-source-tabs button { + flex: 1 1 auto; + } + + .debug-line { + grid-template-columns: 58px 42px 1fr; + } + + .system-panel-head { + align-items: stretch; + flex-direction: column; + } + + .system-health { + align-self: flex-start; + } + + .simulation-mode-banner { + align-items: stretch; + flex-direction: column; + gap: 3px; + } + + .simulation-mode-banner strong { + white-space: normal; + } + + .influx-fields { + grid-template-columns: 1fr; + } + + .influx-fields label:last-child:nth-child(odd) { + grid-column: auto; + } +} + +.history-chart-card canvas { + width: 100%; + min-width: 720px; +} + +.simulation-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.simulation-summary-card { + display: grid; + gap: 6px; + padding: 16px; +} + +.simulation-summary-card small { + color: var(--muted); + font-size: 12px; +} + +.simulation-summary-card strong { + font-size: 28px; + letter-spacing: -.04em; +} + +.simulation-summary-card span { + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.simulation-section-head { + display: flex; + align-items: end; + justify-content: space-between; + margin: 24px 0 12px; +} + +.simulation-section-head h2 { + margin: 4px 0 0; +} + +.simulation-timeline-panel { + padding: 16px; + margin-bottom: 22px; +} + +.simulation-timeline { + display: grid; + gap: 10px; +} + +.simulation-timeline-item { + display: grid; + grid-template-columns: 110px 1fr; + gap: 12px; + align-items: start; + padding: 12px 0; + border-bottom: 1px solid var(--line); +} + +.simulation-timeline-item:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +.simulation-timeline-item time { + color: var(--muted); + font-size: 12px; +} + +.simulation-timeline-item strong { + display: block; + margin-bottom: 4px; +} + +.simulation-timeline-item p { + margin: 0; + color: var(--muted); + line-height: 1.45; +} + +.simulation-zone-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); + gap: 14px; +} + +.simulation-zone-card { + display: grid; + gap: 14px; + padding: 16px; +} + +.simulation-zone-card.disabled { + opacity: .68; +} + +.simulation-zone-head, +.simulation-rule-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.simulation-zone-head h3, +.simulation-rule-head h3 { + margin: 3px 0 0; +} + +.simulation-zone-head p, +.simulation-rule-head p { + margin: 6px 0 0; + color: var(--muted); + font-size: 13px; + line-height: 1.45; +} + +.simulation-flow { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.simulation-step { + display: grid; + gap: 6px; + padding: 12px; + border: 1px solid var(--line); + border-radius: 16px; + background: rgba(255, 255, 255, .02); + min-height: 112px; +} + +html[data-theme='light'] .simulation-step { + background: rgba(0, 0, 0, .02); +} + +.simulation-step.accent { + box-shadow: inset 0 0 0 1px rgba(101, 163, 255, .28); +} + +.simulation-step small, +.simulation-metrics small, +.simulation-rule-meta small { + color: var(--muted); + font-size: 11px; +} + +.simulation-step strong { + font-size: 15px; + line-height: 1.35; +} + +.simulation-step span { + color: var(--muted); + font-size: 12px; + line-height: 1.4; +} + +.simulation-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.simulation-metrics>div, +.simulation-rule-action { + padding: 12px; + border-radius: 14px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, .02); +} + +html[data-theme='light'] .simulation-metrics>div, +html[data-theme='light'] .simulation-rule-action { + background: rgba(0, 0, 0, .02); +} + +.simulation-metrics strong { + display: block; + margin-top: 4px; +} + +.simulation-events { + padding-top: 0; + border-top: 0; +} + +.simulation-rules-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); + gap: 14px; +} + +.simulation-rule-card { + display: grid; + gap: 12px; + padding: 16px; +} + +.simulation-rule-card>p { + margin: 0; + color: var(--muted); +} + +.simulation-rule-action { + font-size: 14px; + line-height: 1.5; +} + +.simulation-rule-meta { + display: flex; + flex-wrap: wrap; + gap: 12px; + color: var(--muted); +} + +@media (max-width: 900px) { + + .simulation-flow, + .simulation-metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .simulation-timeline-item { + grid-template-columns: 1fr; + gap: 6px; + } + + .simulation-flow, + .simulation-metrics { + grid-template-columns: 1fr; + } +} + +/* Compact connection state: a colored dot replaces the Connected/Disconnected label. */ +.brand-line { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.brand-line strong { + display: block; +} + +.connection-dot { + display: inline-block !important; + width: 9px; + height: 9px; + flex: 0 0 9px; + margin: 0 !important; + border-radius: 50%; + background: var(--muted); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--muted) 15%, transparent); +} + +.connection-dot.connected { + background: #37c96b; + box-shadow: 0 0 0 3px rgba(55, 201, 107, .14), 0 0 10px rgba(55, 201, 107, .3); +} + +.connection-dot.disconnected, +.connection-dot.connectionError { + background: #ef5b5b; + box-shadow: 0 0 0 3px rgba(239, 91, 91, .14), 0 0 10px rgba(239, 91, 91, .24); +} + +.connection-dot.connecting { + background: #e2a93b; + box-shadow: 0 0 0 3px rgba(226, 169, 59, .14); + animation: connection-pulse 1.25s ease-in-out infinite; +} + +@keyframes connection-pulse { + 50% { + opacity: .45; + transform: scale(.86); + } +} + +/* v0.5.3 usability refinements */ +.history-toolbar-panel .chart-toolbar { + display: grid; + grid-template-columns: minmax(260px, 1fr) minmax(180px, .55fr) auto; + gap: 12px; + align-items: end; +} + +.history-toolbar-panel .chart-toolbar>* { + min-width: 0; +} + +.history-context-controls { + display: block; + width: 100%; + min-width: 0; +} + +.history-context-controls label, +.history-range-control { + width: 100%; +} + +.history-range-control { + display: grid; + gap: 7px; +} + +.history-refresh-button { + min-height: 45px; + align-self: end; +} + +.settings-form { + display: grid; + gap: 14px; +} + +.settings-mode-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; + margin: 0 0 14px; + padding: 6px; + border: 1px solid var(--line); + border-radius: 17px; + background: var(--surface); +} + +.settings-mode-tabs button { + min-height: 42px; + border: 1px solid transparent; + border-radius: 12px; + color: var(--muted); + background: transparent; + font-size: 12px; + font-weight: 800; +} + +.settings-mode-tabs button.active { + border-color: var(--accent-border); + color: var(--accent); + background: var(--accent-soft); +} + +.settings-pane { + display: grid; + gap: 14px; +} + +.settings-pane[hidden] { + display: none; +} + +.settings-block { + display: grid; + gap: 18px; +} + +.settings-block-head { + display: flex; + align-items: flex-start; + gap: 13px; +} + +.settings-block-head h3 { + margin: 1px 0 4px; + font-size: 18px; +} + +.settings-block-head p { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.5; +} + +.settings-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.settings-grid .wide { + grid-column: 1/-1; +} + +.settings-subhead { + display: flex; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} + +.settings-subhead strong, +.settings-subhead small { + display: block; +} + +.settings-subhead small { + margin-top: 4px; + color: var(--muted); + font-size: 11px; + line-height: 1.4; +} + +.settings-save-bar { + position: sticky; + bottom: 18px; + z-index: 9; + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 13px 15px; + border: 1px solid var(--line-strong); + border-radius: 16px; + background: color-mix(in srgb, var(--surface) 94%, transparent); + box-shadow: 0 8px 30px rgba(0, 0, 0, .18); + backdrop-filter: blur(16px); +} + +.settings-save-bar span { + color: var(--muted); + font-size: 12px; +} + +.sensor-alias-manager { + padding: 14px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--surface-muted); +} + +.sensor-alias-list { + display: grid; + gap: 8px; +} + +.sensor-alias-row { + display: grid; + grid-template-columns: minmax(180px, 1fr) minmax(150px, 1fr) 34px; + gap: 8px; + align-items: center; +} + +.sensor-alias-row>span { + overflow: hidden; + color: var(--muted); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sensor-alias-row input { + min-height: 40px; +} + +.sensor-alias-clear { + width: 34px; + height: 34px; + padding: 0; + border-radius: 10px; + color: var(--muted); + background: transparent; +} + +.sensor-alias-add { + display: grid; + grid-template-columns: minmax(180px, 1fr) minmax(150px, 1fr) auto; + gap: 8px; + margin-top: 10px; +} + +.logs-toolbar { + display: grid; + grid-template-columns: 1fr minmax(160px, 220px) auto; + gap: 14px; + align-items: end; + margin-bottom: 14px; +} + +.logs-toolbar>div strong, +.logs-toolbar>div small { + display: block; +} + +.logs-toolbar>div small { + margin-top: 4px; + color: var(--muted); + font-size: 11px; + line-height: 1.4; +} + +.simulation-flow-shell { + padding: 0; + overflow: hidden; + margin-bottom: 18px; +} + +.simulation-flow-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + padding: 18px 18px 14px; + border-bottom: 1px solid var(--line); +} + +.simulation-flow-head h2 { + margin: 3px 0 0; +} + +.flow-legend { + display: flex; + flex-wrap: wrap; + gap: 12px; + color: var(--muted); + font-size: 11px; +} + +.flow-legend span { + display: flex; + align-items: center; + gap: 6px; +} + +.flow-legend b { + font-weight: 650; +} + +.flow-legend-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--muted); +} + +.flow-legend-dot.input { + background: var(--teal); +} + +.flow-legend-dot.logic { + background: var(--purple); +} + +.flow-legend-dot.action { + background: var(--accent); +} + +.simulation-flow-scroll { + overflow: auto; + max-width: 100%; + scrollbar-width: thin; +} + +.simulation-flow-board { + position: relative; + min-width: 1325px; + background-color: var(--surface-muted); + background-image: radial-gradient(circle, var(--grid) 1px, transparent 1.5px); + background-size: 20px 20px; +} + +.flow-links { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + overflow: visible; +} + +.flow-link { + fill: none; + stroke: color-mix(in srgb, var(--muted) 48%, transparent); + stroke-width: 2; + vector-effect: non-scaling-stroke; +} + +.flow-link.bus { + stroke-dasharray: 5 6; + opacity: .55; +} + +.flow-link.input-link { + stroke: color-mix(in srgb, var(--teal) 65%, var(--muted)); +} + +.flow-link.logic-link { + stroke: color-mix(in srgb, var(--purple) 58%, var(--muted)); +} + +.flow-link.action-link { + stroke: color-mix(in srgb, var(--accent) 60%, var(--muted)); +} + +.flow-link.active-link { + stroke: var(--accent); + stroke-width: 3; + filter: drop-shadow(0 0 4px color-mix(in srgb, var(--accent) 35%, transparent)); +} + +.flow-link.night-link { + stroke: var(--info); + stroke-dasharray: 6 5; + stroke-width: 2.5; +} + +.flow-lane-label { + position: absolute; + z-index: 2; + left: 45px; + width: 1230px; + display: flex; + align-items: center; + gap: 9px; + color: var(--muted); + font-size: 11px; +} + +.flow-lane-label::after { + content: ""; + height: 1px; + flex: 1; + background: var(--line); +} + +.flow-lane-label strong { + color: var(--text-soft); + font-size: 12px; +} + +.flow-node { + position: absolute; + z-index: 3; + display: grid; + align-content: start; + gap: 6px; + padding: 12px 13px; + border: 1px solid var(--line-strong); + border-radius: 12px; + background: color-mix(in srgb, var(--surface) 96%, transparent); + box-shadow: 0 8px 24px rgba(0, 0, 0, .15); +} + +.flow-node.input { + border-left: 3px solid var(--teal); +} + +.flow-node.logic { + border-left: 3px solid var(--purple); +} + +.flow-node.action { + border-left: 3px solid var(--accent); +} + +.flow-node.event { + border-left: 3px solid var(--info); +} + +.flow-node.decision.demand { + box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent), 0 8px 24px rgba(0, 0, 0, .15); +} + +.flow-node.global { + min-height: 106px; +} + +.flow-node.night-active { + border-color: color-mix(in srgb, var(--info) 45%, var(--line-strong)); +} + +.flow-node-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + color: var(--muted); + font-size: 9px; + font-weight: 800; + letter-spacing: .06em; + text-transform: uppercase; +} + +.flow-node h3 { + overflow: hidden; + margin: 0; + color: var(--text); + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.flow-node>strong { + color: var(--text); + font-size: 17px; + line-height: 1.25; +} + +.flow-node p { + display: -webkit-box; + overflow: hidden; + margin: 0; + color: var(--muted); + font-size: 10px; + line-height: 1.35; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.flow-node-badge { + max-width: 86px; + overflow: hidden; + padding: 3px 6px; + border-radius: 999px; + color: var(--muted); + background: var(--surface-muted); + font-size: 8px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.flow-node-badge.active { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, var(--surface)); +} + +.flow-port { + position: absolute; + top: 50%; + width: 9px; + height: 9px; + margin-top: -5px; + border: 2px solid var(--surface); + border-radius: 50%; + background: var(--muted); +} + +.flow-port.in { + left: -6px; +} + +.flow-port.out { + right: -6px; +} + +.flow-node.input .flow-port { + background: var(--teal); +} + +.flow-node.logic .flow-port { + background: var(--purple); +} + +.flow-node.action .flow-port { + background: var(--accent); +} + +.flow-node.event .flow-port { + background: var(--info); +} + +@media (max-width: 760px) { + .history-toolbar-panel .chart-toolbar { + grid-template-columns: 1fr 1fr; + } + + .history-context-controls { + grid-column: 1/-1; + } + + .history-refresh-button { + grid-column: auto; + } + + .settings-grid { + grid-template-columns: 1fr; + } + + .settings-grid .wide { + grid-column: auto; + } + + .settings-save-bar { + bottom: 76px; + } + + .sensor-alias-row, + .sensor-alias-add { + grid-template-columns: 1fr; + } + + .sensor-alias-clear { + justify-self: end; + } + + .logs-toolbar { + grid-template-columns: 1fr; + align-items: stretch; + } + + .simulation-flow-head { + align-items: flex-start; + flex-direction: column; + } +} + +/* v0.5.4: settings headers no longer use decorative icons. */ +.settings-block-head { + gap: 0; +} + +.device-capability-panel { + display: grid; + gap: 8px; + padding: 12px 0 2px; +} + +.device-capability-panel>small { + color: var(--muted); + font-size: 11px; + text-transform: uppercase; + letter-spacing: .08em; +} + +.device-capability-buttons { + display: flex; + flex-wrap: wrap; + gap: 7px; +} + +.device-capability-buttons button { + min-height: 34px; + padding: 7px 11px; + border: 1px solid var(--line); + border-radius: 11px; + background: var(--surface-muted); + color: var(--text); +} + +.device-capability-buttons button.active { + border-color: var(--accent); + color: var(--accent); +} + +/* compact, centered unit-feature controls */ +.device-capability-panel { + justify-items: center; + text-align: center; + padding: 11px 0 3px; +} + +.device-capability-panel>small { + text-align: center; +} + +.device-capability-buttons { + justify-content: center; + align-items: center; + gap: 6px; + max-width: 100%; +} + +.device-capability-buttons button { + min-height: 28px; + padding: 5px 9px; + border-radius: 10px; + color: var(--muted); + font-size: 10px; + line-height: 1.1; + background: transparent; +} + +.device-capability-buttons button:hover { + color: var(--text); + background: var(--surface-muted); +} + +.device-capability-buttons button.active { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, var(--surface)); + border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); +} + +.standalone-settings-form { + max-width: 1100px; +} + +.copy-settings-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: .6rem; + align-items: end +} + +.notification-provider { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .75rem +} + +.log-row { + display: grid; + grid-template-columns: 5.5rem 7rem minmax(9rem, auto) 1fr; + gap: .75rem; + align-items: center; + border-left: 4px solid transparent +} + +.log-row.error { + border-left-color: var(--danger, #d9544d) +} + +.log-row.warn { + border-left-color: #c98a21 +} + +.log-row.info { + border-left-color: var(--accent, #4d9c7d) +} + +.log-category { + font-size: .72rem; + text-transform: uppercase; + letter-spacing: .08em; + opacity: .7 +} + +.category-device .log-category:before { + content: '▣ ' +} + +.category-zone .log-category:before { + content: '◎ ' +} + +.category-automation .log-category:before { + content: '⚙ ' +} + +.category-integration .log-category:before { + content: '↔ ' +} + +.category-settings .log-category:before { + content: '☷ ' +} + +.category-system .log-category:before { + content: '• ' +} + +@media(max-width:720px) { + + .copy-settings-row, + .notification-provider { + grid-template-columns: 1fr + } + + .log-row { + grid-template-columns: 4.5rem 1fr + } + + .log-row .kind, + .log-row .message { + grid-column: 2 + } + + .logs-toolbar { + align-items: stretch + } +} + +/* Group controls and standalone simulator */ +[hidden] { + display: none !important; +} + +.dashboard-group-panel { + display: grid; + gap: 14px; + margin-bottom: 18px; +} + +.dashboard-group-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.dashboard-group-head h3 { + margin: 3px 0 5px; +} + +.dashboard-group-head p { + margin: 0; + color: var(--muted); + line-height: 1.45; +} + +.group-quick-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr)); + gap: 12px; +} + +.group-card { + display: grid; + gap: 13px; +} + +.group-card.group-off { + opacity: .78; +} + +.group-state-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.group-state-summary>div { + padding: 10px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--surface-muted); +} + +.group-state-summary small, +.group-control-block>small { + display: block; + margin-bottom: 5px; + color: var(--muted); + font-size: 10px; + text-transform: uppercase; + letter-spacing: .06em; +} + +.group-state-summary strong { + display: block; + overflow: hidden; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.group-control-block { + display: grid; + gap: 6px; +} + +.group-button-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; +} + +.group-button-row button, +.group-mode-row button, +.group-preset-row button { + min-height: 36px; +} + +.group-button-row button.active { + color: var(--accent-text); + background: var(--accent); + font-weight: 800; +} + +.group-custom-temperature { + padding: 8px 9px; + border: 1px solid var(--line); + border-radius: 11px; + background: var(--surface-muted); +} + +.group-custom-temperature label>span { + display: block; + margin-bottom: 5px; + color: var(--muted); + font-size: 10px; +} + +.group-custom-temperature-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 6px; +} + +.group-custom-temperature-row input { + min-width: 0; + height: 36px; + text-align: center; + font-weight: 700; +} + +.group-custom-temperature-row>span { + color: var(--muted); + font-size: 12px; +} + +.group-custom-temperature-row button { + min-height: 36px; + white-space: nowrap; +} + +.group-zone-choices { + display: grid; + gap: 7px; + max-height: 300px; + overflow: auto; +} + +.group-zone-choice { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + gap: 10px; + padding: 10px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--surface-muted); +} + +.group-zone-choice input { + width: 18px; + min-height: 18px; + accent-color: var(--accent); +} + +.group-zone-choice strong, +.group-zone-choice small { + display: block; +} + +.group-zone-choice small { + margin-top: 3px; + color: var(--muted); + font-size: 10px; +} + +.simulation-page-head { + align-items: flex-start; +} + +.simulation-head-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; +} + +.simulation-scope-toolbar { + display: flex; + align-items: end; + gap: 12px; + margin-bottom: 16px; + padding: 14px; +} + +.simulation-scope-toolbar label { + min-width: min(260px, 100%); +} + +body.simulation-standalone .topbar, +body.simulation-standalone .bottom-nav { + display: none !important; +} + +body.simulation-standalone main { + width: 100%; + max-width: none; + margin: 0; + padding: 18px; +} + +body.simulation-standalone [data-view="simulation"] { + min-height: calc(100vh - 36px); +} + +:fullscreen main { + width: 100%; + max-width: none; + padding: 18px; +} + +:fullscreen .topbar, +:fullscreen .bottom-nav { + display: none !important; +} + +@media (max-width:720px) { + + .dashboard-group-head, + .simulation-page-head { + align-items: stretch; + flex-direction: column; + } + + .simulation-head-actions { + justify-content: stretch; + } + + .simulation-head-actions button { + flex: 1 1 180px; + } + + .simulation-scope-toolbar { + align-items: stretch; + flex-direction: column; + } + + .group-state-summary { + grid-template-columns: 1fr; + } +} + +/* Dashboard hierarchy: compact global controls -> wrapped groups -> on-demand details. */ +[data-view="dashboard"] .hero { + min-height: 164px; + padding: 24px 28px; + border-radius: 24px; +} + +[data-view="dashboard"] .hero h1 { + margin-bottom: 6px; +} + +[data-view="dashboard"] .hero-temp { + font-size: clamp(44px, 8vw, 68px); +} + +[data-view="dashboard"] .metrics { + gap: 10px; + margin: 10px 0 20px; +} + +[data-view="dashboard"] .metric { + padding: 14px 16px; + border-radius: 15px; +} + +[data-view="dashboard"] .metric span { + margin-bottom: 5px; +} + +[data-view="dashboard"] .metric strong { + font-size: 21px; +} + +.dashboard-global-panel { + display: grid; + gap: 14px; + padding: 16px 18px; + margin: 0 0 20px; +} + +.dashboard-global-power { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.dashboard-global-power h3 { + margin: 2px 0 3px; + font-size: 18px; +} + +.dashboard-global-power p { + margin: 0; + max-width: 680px; + color: var(--muted); + font-size: 11px; + line-height: 1.4; +} + +.dashboard-global-divider { + height: 1px; + background: var(--line); +} + +.dashboard-house-climate { + margin: 0; + gap: 12px; +} + +.dashboard-house-climate .house-climate-head h3 { + font-size: 18px; +} + +.dashboard-house-climate .house-climate-head p { + font-size: 11px; +} + +.dashboard-house-climate .outside-pill { + padding: 8px 11px; +} + +.dashboard-house-climate .house-mode-row button { + min-height: 38px; + padding: 8px 10px; +} + +.dashboard-house-climate .house-preset-row { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 6px; + overflow: visible; + padding: 0; +} + +.dashboard-house-climate .house-preset-row button { + min-width: 0; + min-height: 36px; + padding: 7px 8px; + font-size: 11px; +} + +.dashboard-primary-section { + margin-bottom: 18px; +} + +.dashboard-section-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin: 0 0 10px; +} + +.dashboard-section-head h2 { + margin: 3px 0 4px; + font-size: 21px; +} + +.dashboard-section-head p { + margin: 0; + max-width: 720px; + color: var(--muted); + font-size: 11px; + line-height: 1.4; +} + +.dashboard-section-actions { + display: flex; + align-items: center; + flex: 0 0 auto; + gap: 7px; +} + +.dashboard-section-actions .badge { + min-width: 28px; + justify-content: center; +} + +.dashboard-section-actions button { + min-height: 36px; + padding: 8px 11px; +} + +/* Groups wrap into rows. Horizontal scrolling is intentionally disabled on the dashboard. */ +.dashboard-group-strip { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); + gap: 10px; + overflow: visible; + padding: 0; +} + +.dashboard-group-strip .group-card { + min-width: 0; + padding: 13px; + gap: 8px; +} + +.dashboard-group-strip .list-card-head { + gap: 9px; +} + +.dashboard-group-strip .list-card-head h3 { + font-size: 16px; +} + +.dashboard-group-strip .list-card-head p { + display: -webkit-box; + overflow: hidden; + max-width: none; + font-size: 11px; + line-height: 1.35; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + white-space: normal; +} + +.dashboard-group-strip .badge { + padding: 4px 7px; + font-size: 10px; +} + +.dashboard-group-strip .group-control-block { + gap: 4px; +} + +.dashboard-group-strip .group-control-block>small { + margin-bottom: 1px; + font-size: 9px; +} + +.dashboard-group-strip .group-button-row { + gap: 5px; +} + +.dashboard-group-strip .group-mode-row, +.dashboard-group-strip .group-preset-row { + display: grid; + gap: 5px; + overflow: visible; + padding: 0; +} + +.dashboard-group-strip .group-mode-row { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.dashboard-group-strip .group-preset-row { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.dashboard-group-strip .group-button-row button, +.dashboard-group-strip .group-mode-row button, +.dashboard-group-strip .group-preset-row button { + min-width: 0; + min-height: 32px; + padding: 6px 5px; + font-size: 10px; + line-height: 1.15; +} + +.dashboard-secondary-stack { + display: grid; + gap: 8px; + margin-top: 4px; +} + +.dashboard-disclosure { + overflow: hidden; + padding: 0; +} + +.dashboard-disclosure>summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 13px 15px; + cursor: pointer; + list-style: none; + user-select: none; +} + +.dashboard-disclosure>summary::-webkit-details-marker { + display: none; +} + +.dashboard-disclosure>summary h3 { + margin: 2px 0 0; + font-size: 16px; +} + +.dashboard-disclosure>summary .eyebrow { + font-size: 9px; +} + +.dashboard-summary-side { + display: flex; + align-items: center; + gap: 10px; +} + +.dashboard-summary-side .badge { + min-width: 28px; + justify-content: center; +} + +.dashboard-disclosure-chevron { + display: inline-block; + color: var(--muted); + font-size: 18px; + line-height: 1; + transition: transform .18s ease; +} + +.dashboard-disclosure[open]>summary { + border-bottom: 1px solid var(--line); +} + +.dashboard-disclosure[open] .dashboard-disclosure-chevron { + transform: rotate(180deg); +} + +.dashboard-disclosure-body { + padding: 12px; +} + +.dashboard-disclosure-body .automation-plan-grid, +.dashboard-disclosure-body .dashboard-zones { + margin-bottom: 0; +} + +.dashboard-disclosure-body .device-grid, +.dashboard-disclosure-body .list-grid, +.dashboard-disclosure-body .automation-plan-grid { + gap: 10px; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); +} + +/* Dashboard quick controls share one compact visual language. */ +.quick-control-card { + --quick-control-height: 28px; + --quick-control-gap: 4px; + padding: 14px; +} + +.quick-control-card .quick-control-row { + display: grid; + gap: var(--quick-control-gap); + overflow: visible; + padding: 2px 0 5px; +} + +.quick-control-card .quick-control-row-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.quick-control-card .quick-control-row-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.quick-control-card .quick-control-row-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.quick-control-card .quick-control-row-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +/* Polish mode labels are noticeably wider than Auto/Nawiew/Grzanie. + Give Cool/Dry more room instead of shrinking or clipping their text. */ +.quick-device-control .mode-row.quick-control-row-5 { + grid-template-columns: minmax(0, .8fr) minmax(0, 1.35fr) minmax(0, 1.2fr) minmax(0, .85fr) minmax(0, .8fr); +} + +.quick-device-control .mode-row.quick-control-row-5 button { + padding-inline: 3px; +} + +.quick-control-card .quick-control-row button { + min-width: 0; + min-height: var(--quick-control-height); + padding: 4px 5px; + font-size: 9px; + line-height: 1.05; + white-space: nowrap; +} + +.quick-control-card .power-button { + width: 38px; + height: 38px; + font-size: 18px; +} + +.quick-control-card .temperature-control button { + width: 34px; + height: 34px; + font-size: 18px; +} + +.quick-control-card .target-temp { + min-width: 80px; + font-size: 32px; +} + +.quick-control-card .device-title h3, +.quick-control-card .list-card-head h3 { + font-size: 16px; +} + +.quick-control-card .device-title p, +.quick-control-card .list-card-head p { + font-size: 11px; +} + +.quick-device-control .temperature-control { + gap: 12px; + padding: 12px 0 10px; +} + +.quick-device-control .current-line { + margin: -3px 0 10px; + font-size: 11px; +} + +.quick-device-control .device-toggles.quick-control-row { + margin-top: 1px; +} + +.quick-thermostat-control .zone-quick-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 5px; + min-width: 128px; +} + +.quick-thermostat-control .zone-quick-actions button { + display: flex; + align-items: center; + justify-content: center; + gap: 5px; + min-width: 0; + min-height: 30px; + padding: 5px 7px; + font-size: 10px; + white-space: nowrap; +} + +.quick-thermostat-control .zone-quick-actions button span { + font-size: 13px; + line-height: 1; +} + +.quick-thermostat-control .zone-quick-actions button b { + overflow: hidden; + font-size: 10px; + font-variant-numeric: tabular-nums; + text-overflow: ellipsis; +} + +.quick-thermostat-control .zone-unit-power-toggle.active { + color: var(--accent-text); + background: var(--accent); + font-weight: 800; +} + +.quick-thermostat-control .temporary-thermostat-toggle.active { + border-color: color-mix(in srgb, var(--warning) 55%, var(--line)); + color: var(--warning); + background: var(--warning-soft); +} + +.quick-thermostat-control .thermostat-main { + margin: 8px 0 7px; +} + +.zone-config-card .zone-enable-toggle { + min-height: 28px; + padding: 4px 8px; + font-size: 10px; +} + +.temporary-thermostat-form { + gap: 16px; +} + +.temporary-rule-fields { + display: grid; + gap: 12px; + padding: 13px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--surface-muted); +} + +.temporary-rule-fields[hidden] { + display: none; +} + +.temporary-presets { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 5px; +} + +.temporary-presets button { + min-height: 32px; + padding: 6px; + font-size: 10px; +} + +.temporary-active-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 13px; + border: 1px solid color-mix(in srgb, var(--warning) 45%, var(--line)); + border-radius: 14px; + background: var(--warning-soft); +} + +.temporary-active-summary[hidden] { + display: none; +} + +.temporary-active-summary>div { + display: grid; + gap: 3px; + min-width: 0; +} + +.temporary-active-summary small, +.temporary-active-summary span { + color: var(--muted); + font-size: 10px; + line-height: 1.35; +} + +.temporary-active-summary strong { + color: var(--warning); + font-size: 22px; + font-variant-numeric: tabular-nums; + letter-spacing: .02em; +} + +.temporary-active-summary .danger { + flex: 0 0 auto; +} + +@media (max-width: 700px) { + [data-view="dashboard"] .hero { + min-height: 145px; + padding: 20px; + } + + [data-view="dashboard"] .metrics { + margin-bottom: 16px; + } + + [data-view="dashboard"] .metric { + padding: 12px; + } + + .dashboard-global-panel { + padding: 14px; + margin-bottom: 18px; + } + + .dashboard-global-power { + align-items: stretch; + flex-direction: column; + } + + .dashboard-global-power .house-power-actions { + display: grid; + grid-template-columns: 1fr 1fr; + } + + .dashboard-house-climate .house-preset-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .dashboard-section-head { + align-items: stretch; + flex-direction: column; + gap: 10px; + } + + .dashboard-section-actions { + justify-content: space-between; + } + + .dashboard-section-actions button { + flex: 1; + } + + .dashboard-group-strip { + grid-template-columns: 1fr; + } + + .dashboard-group-strip .group-preset-row { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .dashboard-disclosure>summary { + padding: 12px 13px; + } + + .dashboard-disclosure-body { + padding: 10px; + } + + .quick-thermostat-control .list-card-head { + align-items: center; + } + + .quick-thermostat-control .zone-quick-actions { + min-width: 116px; + } + + .temporary-presets { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .temporary-active-summary { + align-items: stretch; + flex-direction: column; + } + + .temporary-active-summary .danger { + width: 100%; + white-space: normal; + } +} + +/* Dashboard sections: horizontal tabs, one vertically scrollable panel at a time. */ +.dashboard-tabs { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin: 0 0 12px; + padding: 6px; + border: 1px solid var(--line); + border-radius: 17px; + background: var(--surface); +} + +.dashboard-tabs button { + min-width: 0; + min-height: 40px; + padding: 8px 10px; + border: 1px solid transparent; + border-radius: 12px; + color: var(--muted); + background: transparent; + font-size: 12px; + font-weight: 750; +} + +.dashboard-tabs button.active { + border-color: var(--accent-border); + color: var(--accent); + background: var(--accent-soft); +} + +.dashboard-tab-panel[hidden] { + display: none !important; +} + +.dashboard-tab-panel.active { + display: block; + animation: reveal .18s ease; +} + +.dashboard-section-panel { + margin-bottom: 18px; +} + +.dashboard-section-head.compact-head { + align-items: center; + margin-bottom: 12px; +} + +.dashboard-section-head.compact-head h2 { + font-size: 19px; +} + +.dashboard-section-panel>.device-grid, +.dashboard-section-panel>.list-grid, +.dashboard-section-panel>.automation-plan-grid { + margin-bottom: 0; +} + +.notification-alert-types { + display: grid; + gap: 10px; +} + +.notification-alert-types>.field-note { + margin: 0; +} + +.notification-alert-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px 14px; +} + +.notification-alert-grid .check { + min-height: 36px; +} + +@media (max-width:700px) { + .topbar { + gap: 7px; + padding-inline: 10px; + } + + .toolbar-runtime-status { + padding: 6px 8px; + font-size: 10px; + } + + .section-heading { + align-items: stretch; + flex-direction: column; + } + + .section-heading-actions { + display: grid; + grid-template-columns: 1fr 1fr; + width: 100%; + } + + .dashboard-tabs { + gap: 5px; + padding: 5px; + } + + .dashboard-tabs button { + min-height: 42px; + padding: 6px 5px; + font-size: 10px; + line-height: 1.15; + } + + .notification-alert-grid { + grid-template-columns: 1fr; + } +} + +/* 0.6.9 dashboard density and fit fixes */ +[data-view="dashboard"] .dashboard-summary-card { + min-height: 0; + align-items: center; + gap: 14px; + padding: 13px 16px; + border-radius: 18px; +} + +[data-view="dashboard"] .dashboard-summary-card::after { + width: 100px; + height: 100px; + right: -44px; + bottom: -64px; + border-width: 18px; +} + +[data-view="dashboard"] .dashboard-summary-copy { + min-width: 0; +} + +[data-view="dashboard"] .dashboard-summary-copy .eyebrow { + font-size: 9px; +} + +[data-view="dashboard"] .dashboard-summary-copy h1 { + margin: 2px 0 3px; + font-size: 20px; + letter-spacing: -.025em; +} + +[data-view="dashboard"] .dashboard-summary-copy p { + font-size: 11px; + line-height: 1.3; +} + +[data-view="dashboard"] .dashboard-summary-status { + z-index: 1; + display: flex; + align-items: center; + justify-content: flex-end; + min-width: 0; + gap: 10px; +} + +[data-view="dashboard"] .dashboard-summary-status .metrics { + display: flex; + align-items: center; + gap: 5px; + margin: 0; +} + +[data-view="dashboard"] .dashboard-summary-status .metric { + display: flex; + align-items: baseline; + gap: 5px; + min-width: 0; + padding: 6px 8px; + border-radius: 11px; + background: var(--surface-muted); +} + +[data-view="dashboard"] .dashboard-summary-status .metric span { + margin: 0; + font-size: 9px; + white-space: nowrap; +} + +[data-view="dashboard"] .dashboard-summary-status .metric strong { + font-size: 13px; + line-height: 1; +} + +[data-view="dashboard"] .dashboard-summary-status .hero-temp { + min-width: 72px; + font-size: 31px; + letter-spacing: -.045em; + text-align: right; +} + +/* Quick thermostat: target control gets its own row, readings no longer collide with it. */ +#dashboardZones { + grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); +} + +#dashboardZones, +#dashboardDevices { + align-items: start; +} + +.quick-thermostat-control { + min-width: 0; +} + +.quick-thermostat-control .thermostat-main { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + grid-template-areas: "control control" "room device"; + gap: 5px 10px; + margin: 8px 0 7px; +} + +.quick-thermostat-control .thermostat-main>div:first-child { + grid-area: room; + min-width: 0; + text-align: left; +} + +.quick-thermostat-control .thermostat-main>.temperature-control { + grid-area: control; +} + +.quick-thermostat-control .thermostat-main>div:last-child { + grid-area: device; + min-width: 0; + text-align: right; +} + +.quick-thermostat-control .thermostat-main small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.quick-thermostat-control .thermostat-main strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; +} + +.quick-thermostat-control .thermostat-main .temperature-control.compact { + gap: 12px; + padding: 0 0 2px; +} + +.quick-thermostat-control .thermostat-main .temperature-control.compact button { + width: 34px; + height: 34px; + font-size: 18px; +} + +.quick-thermostat-control .thermostat-main .target-temp.compact { + min-width: 80px; + font-size: 32px; +} + +.quick-thermostat-control .list-card-head h3 { + font-size: 16px; +} + +.quick-thermostat-control .list-card-head p { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; +} + +.quick-thermostat-control .zone-enable-toggle { + flex: 0 0 auto; +} + +.quick-thermostat-control .zone-state-line { + gap: 8px; + font-size: 10px; +} + +.quick-thermostat-control .zone-state-line span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.quick-thermostat-control .control-owner-line { + align-items: flex-start; + flex-wrap: wrap; +} + +.quick-thermostat-control .control-owner-line span { + overflow: visible; + text-overflow: clip; + white-space: normal; +} + +/* Smaller Node-RED/simulator-like plan blocks with explicit group context. */ +#controlPlan.automation-plan-grid { + grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); + gap: 8px; +} + +#controlPlan .plan-card { + min-height: 0; + gap: 7px; + padding: 11px; + border-radius: 14px; +} + +#controlPlan .plan-card-head { + gap: 7px; +} + +#controlPlan .plan-card-head h3 { + margin-top: 1px; + font-size: 15px; + line-height: 1.2; +} + +#controlPlan .plan-card-head .eyebrow { + font-size: 8px; + line-height: 1.25; +} + +#controlPlan .plan-card .badge { + max-width: 100px; + padding: 4px 6px; + overflow: hidden; + font-size: 9px; + text-overflow: ellipsis; + white-space: nowrap; +} + +#controlPlan .plan-card>p { + font-size: 10px; + line-height: 1.3; +} + +#controlPlan .plan-temp { + gap: 5px; +} + +#controlPlan .plan-temp>span { + font-size: 15px; +} + +#controlPlan .plan-temp>strong { + font-size: 21px; +} + +#controlPlan .plan-group-state { + display: flex; + align-items: center; + gap: 5px; + min-width: 0; + color: var(--text-soft); + font-size: 11px; +} + +#controlPlan .plan-group-state strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#controlPlan .plan-group { + border-color: color-mix(in srgb, var(--accent) 20%, var(--line)); + background: color-mix(in srgb, var(--surface) 97%, var(--accent)); +} + +#controlPlan .plan-events { + gap: 4px; + padding-top: 6px; +} + +#controlPlan .plan-events li { + grid-template-columns: 62px minmax(0, 1fr); + gap: 5px; + font-size: 9px; + line-height: 1.3; +} + +#controlPlan .plan-events li span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#controlPlan .plan-rules { + gap: 10px; +} + +#controlPlan .plan-rules .plan-card-head { + padding-bottom: 2px; +} + +#controlPlan .plan-rules .plan-events { + gap: 8px; + padding-top: 9px; +} + +#controlPlan .plan-rules .plan-events li { + align-items: start; + gap: 8px; +} + +#controlPlan .plan-events .plan-rule-summary { + display: grid; + gap: 2px; + overflow: visible; + text-overflow: clip; + white-space: normal; +} + +#controlPlan .plan-rule-summary strong { + color: var(--text); + font-size: 10px; + font-weight: 650; + line-height: 1.35; +} + +#controlPlan .plan-rule-summary small { + color: var(--muted); + font-size: 9px; + line-height: 1.35; +} + +#controlPlan .plan-events .muted { + white-space: normal; +} + +@media (max-width: 900px) { + [data-view="dashboard"] .dashboard-summary-card { + align-items: flex-start; + } + + [data-view="dashboard"] .dashboard-summary-status { + flex-wrap: wrap; + } + + [data-view="dashboard"] .dashboard-summary-status .metrics { + order: 2; + width: 100%; + justify-content: flex-end; + } +} + +@media (max-width: 700px) { + [data-view="dashboard"] .dashboard-summary-card { + min-height: 0; + padding: 12px 13px; + } + + [data-view="dashboard"] .dashboard-summary-copy h1 { + font-size: 18px; + } + + [data-view="dashboard"] .dashboard-summary-status { + align-self: center; + } + + [data-view="dashboard"] .dashboard-summary-status .metrics { + display: none; + } + + [data-view="dashboard"] .dashboard-summary-status .hero-temp { + min-width: 64px; + font-size: 27px; + } + + #dashboardZones { + grid-template-columns: 1fr; + } + + #controlPlan.automation-plan-grid { + grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); + } +} + +[data-view="dashboard"] .dashboard-summary-card { + margin-bottom: 10px; +} + +/* 0.7.2: Zones are configuration cards; live controls stay on the Dashboard. */ +.zone-config-card { + display: grid; + gap: 12px; + overflow: hidden; +} + +.zone-config-card.demanding { + border-color: color-mix(in srgb, var(--accent) 38%, var(--line)); +} + +.zone-config-card.zone-disabled .zone-config-status { + opacity: .68; +} + +.zone-config-status { + display: grid; + grid-template-columns: minmax(150px, .8fr) minmax(180px, 1.2fr); + align-items: center; + gap: 12px; + padding: 11px 12px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--surface-muted); +} + +.zone-config-temperature>small, +.zone-config-grid small { + display: block; + margin-bottom: 4px; + color: var(--muted); + font-size: 9px; + text-transform: uppercase; + letter-spacing: .06em; +} + +.zone-config-temperature>div { + display: flex; + align-items: baseline; + gap: 7px; +} + +.zone-config-temperature span { + color: var(--muted); + font-size: 18px; +} + +.zone-config-temperature b { + color: var(--muted); + font-size: 11px; +} + +.zone-config-temperature strong { + font-size: 23px; + letter-spacing: -.03em; +} + +.zone-config-runtime { + display: grid; + justify-items: end; + gap: 4px; + min-width: 0; + color: var(--text-soft); + font-size: 11px; + text-align: right; +} + +.zone-config-runtime>span:not(.badge), +.zone-config-runtime small { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.zone-config-runtime small { + color: var(--muted); +} + +.zone-config-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; +} + +.zone-config-grid>div { + min-width: 0; + padding: 9px 10px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--surface-muted); +} + +.zone-config-grid strong { + display: block; + overflow: hidden; + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +#zoneList .zone-config-card .sensor-detail { + margin-top: 0; +} + +#zoneList .zone-config-card .card-footer { + align-items: flex-start; + margin-top: 0; +} + +#zoneList .zone-config-card .card-footer>small { + flex: 1 1 auto; + min-width: 0; + overflow: visible; + text-overflow: clip; + white-space: normal; + line-height: 1.35; +} + +#zoneList .zone-config-card .card-menu { + flex: 0 0 auto; + flex-wrap: wrap; + justify-content: flex-end; +} + +#zoneList .zone-config-card .card-menu .primary { + color: var(--accent-text); + background: var(--accent); +} + +@media (max-width:620px) { + .zone-config-status { + grid-template-columns: 1fr; + } + + .zone-config-runtime { + justify-items: start; + text-align: left; + } + + .zone-config-grid { + grid-template-columns: 1fr; + } + + #zoneList .zone-config-card .card-footer { + flex-direction: column; + } + + #zoneList .zone-config-card .card-menu { + width: 100%; + justify-content: flex-start; + } +} + +/* 0.7.1+: quick thermostats and quick manual control use the shared quick-control classes above. */ + +/* 0.8.11 mobile toolbar and chart inspection UX */ +.topbar { + grid-template-columns: minmax(0, 1fr) auto auto; + column-gap: 10px; +} + +.top-actions { + min-width: 0; +} + +.language-picker { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + width: 58px; + height: 40px; + border: 1px solid var(--line); + border-radius: 11px; + background: var(--surface-muted); + color: var(--text-soft); + transition: border-color .15s ease, background .15s ease, transform .15s ease; +} + +.language-picker:hover { + background: var(--surface-2); +} + +.language-picker:active { + transform: scale(.97); +} + +.language-label { + font-size: 11px; + font-weight: 850; + letter-spacing: .06em; + line-height: 1; +} + +.language-chevron { + margin-top: -2px; + color: var(--muted); + font-size: 13px; + line-height: 1; +} + +.language-select { + position: absolute; + inset: 0; + width: 100%; + min-width: 0; + height: 100%; + min-height: 0; + margin: 0; + padding: 0; + border: 0; + opacity: 0; + cursor: pointer; +} + +.toolbar-icon-control { + display: inline-grid; + place-items: center; + flex: 0 0 40px; + width: 40px; + height: 40px; + min-height: 40px; + padding: 0; + border: 1px solid var(--line); + border-radius: 11px; + background: var(--surface-muted); + color: var(--text-soft); + font-size: 18px; + line-height: 1; +} + +.toolbar-icon-control:hover { + background: var(--surface-2); +} + +#refreshButton.toolbar-icon-control { + font-size: 21px; +} + +button:focus-visible, +.language-picker:focus-within { + outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); + outline-offset: 2px; +} + +.chart-hover-line { + position: absolute; + z-index: 3; + width: 1px; + background: color-mix(in srgb, var(--text) 40%, transparent); + pointer-events: none; +} + +.chart-tooltip { + position: absolute; + z-index: 5; + min-width: 175px; + max-width: min(290px, calc(100vw - 32px)); + padding: 10px 11px; + border: 1px solid var(--line); + border-radius: 12px; + background: color-mix(in srgb, var(--surface) 96%, transparent); + box-shadow: 0 10px 28px rgba(0, 0, 0, .2); + color: var(--text); + font-size: 11px; + line-height: 1.35; + pointer-events: none; + backdrop-filter: blur(12px); +} + +.chart-tooltip-time { + display: block; + margin-bottom: 7px; + color: var(--text-soft); + font-weight: 800; + font-variant-numeric: tabular-nums; +} + +.chart-tooltip-values { + display: grid; + gap: 5px; +} + +.chart-tooltip-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; +} + +.chart-tooltip-label { + display: inline-flex; + min-width: 0; + align-items: center; + gap: 6px; + color: var(--muted); +} + +.chart-tooltip-label i { + flex: 0 0 7px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--tooltip-color); +} + +.chart-tooltip-label span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chart-tooltip-value { + color: var(--text); + font-weight: 800; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.chart-tooltip-sample-time { + color: var(--muted); + font-size: 9px; + font-weight: 600; +} + +@media (max-width: 700px) { + .topbar { + min-height: 64px; + gap: 6px; + padding: 9px 10px; + } + + .brand strong { + font-size: 13px; + } + + .toolbar-runtime-status { + padding: 6px 8px; + font-size: 10px; + } + + .top-actions { + gap: 5px; + } + + .language-picker { + width: 50px; + height: 38px; + border-radius: 10px; + } + + .toolbar-icon-control { + flex-basis: 38px; + width: 38px; + height: 38px; + min-height: 38px; + border-radius: 10px; + } + + .language-chevron { + font-size: 11px; + } +} + +@media (max-width: 360px) { + .topbar { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + } + + .brand { + grid-column: 1; + grid-row: 1; + } + + .top-actions { + grid-column: 2; + grid-row: 1; + } + + .toolbar-runtime-status { + grid-column: 1 / -1; + grid-row: 2; + justify-self: stretch; + text-align: center; + } +} + +.chart-tooltip-sample-time { + display: block; + margin-top: 1px; +} + +@media (max-width: 700px) { + .top-actions { + width: auto; + } +} + +.chart-hover-hint { + display: block; + margin-top: 5px; + color: var(--muted); + font-size: 10px; +} + +.chart-tooltip-value { + text-align: right; +} + +.chart-wrap canvas:focus-visible { + outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); + outline-offset: -2px; +} + +.toolbar-runtime-status { + min-width: max-content; +} + +/* 0.8.13 settings separation, simulation warning, system status and debug filters */ +.toolbar-picker { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + flex: 0 0 54px; + width: 54px; + height: 40px; + border: 1px solid var(--line); + border-radius: 11px; + background: var(--surface-muted); + color: var(--text-soft); + transition: border-color .15s ease, background .15s ease, transform .15s ease; +} + +.toolbar-picker:hover { + background: var(--surface-2); +} + +.toolbar-picker:active { + transform: scale(.97); +} + +.toolbar-picker:focus-within { + outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); + outline-offset: 2px; +} + +.toolbar-picker-value { + min-width: 18px; + color: var(--text-soft); + font-size: 11px; + font-weight: 850; + letter-spacing: .06em; + line-height: 1; + text-align: center; +} + +.theme-picker-value { + font-size: 16px; + letter-spacing: 0; +} + +.toolbar-picker-chevron { + margin-top: -2px; + color: var(--muted); + font-size: 12px; + line-height: 1; +} + +.toolbar-picker-select { + position: absolute; + inset: 0; + width: 100%; + min-width: 0; + height: 100%; + min-height: 0; + margin: 0; + padding: 0; + border: 0; + opacity: 0; + cursor: pointer; +} + +.connection-banner { + grid-column: 1 / -1; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + width: 100%; + padding: 9px 11px; + border: 1px solid color-mix(in srgb, var(--danger) 34%, var(--line)); + border-radius: 13px; + background: color-mix(in srgb, var(--danger-soft) 78%, var(--surface)); +} + +.connection-banner[hidden] { + display: none; +} + +.simulation-mode-banner { + grid-column: 1 / -1; + display: flex; + align-items: center; + justify-content: center; + gap: 14px; + width: 100%; + padding: 13px 16px; + border: 2px solid color-mix(in srgb, var(--warning) 62%, var(--line)); + border-radius: 14px; + background: color-mix(in srgb, var(--warning-soft) 90%, var(--surface)); + text-align: center; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--warning) 10%, transparent), 0 5px 18px color-mix(in srgb, var(--warning) 10%, transparent); +} + +.simulation-mode-banner[hidden] { + display: none; +} + +.simulation-mode-banner strong { + color: var(--warning); + font-size: clamp(18px, 2.6vw, 26px); + font-weight: 950; + letter-spacing: .09em; + line-height: 1; + white-space: nowrap; +} + +.simulation-mode-banner span { + max-width: 720px; + color: var(--text-soft); + font-size: 12px; + font-weight: 700; + line-height: 1.35; +} + +.connection-banner-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--danger); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 13%, transparent); +} + +.connection-banner>div { + min-width: 0; +} + +.connection-banner strong, +.connection-banner span { + display: block; +} + +.connection-banner strong { + margin-bottom: 1px; + font-size: 11px; +} + +.connection-banner>div span { + overflow: hidden; + color: var(--muted); + font-size: 10px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.connection-banner button { + min-height: 32px; + padding: 6px 10px; + border-radius: 9px; + font-size: 10px; +} + +body.connection-lost .connection-dot { + box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 13%, transparent); +} + +.field-error { + display: block; + margin-top: -2px; + color: var(--danger); + font-size: 10px; + font-weight: 650; + line-height: 1.35; +} + +.has-error>input, +.has-error>select, +.has-error>textarea, +input[aria-invalid="true"], +select[aria-invalid="true"], +textarea[aria-invalid="true"] { + border-color: color-mix(in srgb, var(--danger) 68%, var(--line)); + outline: 2px solid color-mix(in srgb, var(--danger) 13%, transparent); +} + +.form-error-summary { + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--danger) 36%, var(--line)); + border-radius: 12px; + background: var(--danger-soft); + color: var(--danger); + font-size: 11px; + font-weight: 650; + line-height: 1.4; +} + +.settings-form>.form-error-summary { + margin: 0 0 12px; +} + +.dialog-form>.form-error-summary { + margin-top: -2px; +} + +.settings-form.is-saving, +.dialog-form.is-saving { + cursor: progress; +} + +.button-spinner { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 7px; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + vertical-align: -2px; + animation: button-spin .7s linear infinite; +} + +@keyframes button-spin { + to { + transform: rotate(360deg); + } +} + +.chart-title-row { + align-items: flex-start; +} + +.chart-zoom-controls { + display: inline-grid; + grid-template-columns: 32px minmax(52px, auto) 32px; + flex: 0 0 auto; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--surface-muted); +} + +.chart-zoom-controls button { + min-width: 0; + height: 30px; + padding: 0 7px; + border-radius: 0; + border-right: 1px solid var(--line); + background: transparent; + color: var(--text-soft); + font-size: 12px; +} + +.chart-zoom-controls button:last-child { + border-right: 0; +} + +.chart-zoom-controls button:hover:not(:disabled) { + background: var(--surface-2); +} + +.chart-zoom-controls button:disabled { + cursor: default; + opacity: .34; +} + +.chart-zoom-reset { + color: var(--muted) !important; + font-size: 10px !important; + font-variant-numeric: tabular-nums; +} + +.chart-wrap { + overscroll-behavior-inline: contain; + scroll-behavior: smooth; +} + +.chart-wrap canvas { + touch-action: pan-y; + user-select: none; + -webkit-user-select: none; +} + +.chart-selection { + position: absolute; + z-index: 4; + border: 1px solid color-mix(in srgb, var(--accent) 76%, transparent); + border-radius: 4px; + background: color-mix(in srgb, var(--accent) 16%, transparent); + pointer-events: none; +} + +@media (hover:hover) and (pointer:fine) { + .history-chart-card .chart-wrap canvas { + cursor: crosshair; + } +} + +@media (max-width: 700px) { + .toolbar-picker { + flex-basis: 38px; + width: 38px; + height: 38px; + border-radius: 10px; + gap: 2px; + } + + .toolbar-picker-value { + min-width: 15px; + font-size: 10px; + } + + .theme-picker-value { + font-size: 14px; + } + + .toolbar-picker-chevron { + font-size: 9px; + } + + .connection-banner { + grid-template-columns: auto minmax(0, 1fr) auto; + padding: 8px 9px; + } + + .connection-banner>div span { + white-space: normal; + } + + .chart-title-row { + align-items: stretch; + flex-direction: column; + } + + .chart-zoom-controls { + align-self: flex-end; + } +} + +@media (max-width: 360px) { + .connection-banner { + grid-column: 1 / -1; + } +} + +@media (max-width: 460px) { + .topbar { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + } + + .brand { + grid-column: 1; + grid-row: 1; + } + + .top-actions { + grid-column: 2; + grid-row: 1; + } + + .toolbar-runtime-status { + grid-column: 1 / -1; + grid-row: 2; + justify-self: stretch; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + } + + .connection-banner { + grid-column: 1 / -1; + grid-row: auto; + } +} + +.settings-form.has-unsaved-changes .settings-save-bar { + border-color: color-mix(in srgb, var(--accent) 48%, var(--line-strong)); + box-shadow: 0 8px 30px rgba(0, 0, 0, .18), 0 0 0 1px color-mix(in srgb, var(--accent) 10%, transparent); +} + +/* Standalone HTTP error pages */ +.error-page { + min-height: 100dvh; +} + +.error-topbar { + grid-template-columns: 1fr; +} + +.error-main { + display: grid; + place-items: center; + min-height: calc(100dvh - 72px); + padding: 32px 18px max(48px, env(safe-area-inset-bottom)); +} + +.error-card { + width: min(620px, 100%); + padding: clamp(28px, 7vw, 52px); + border: 1px solid color-mix(in srgb, var(--accent) 24%, var(--line)); + border-radius: 30px; + background: var(--surface); + box-shadow: 0 24px 80px rgba(0, 0, 0, .18); +} + +.error-code { + margin: 12px 0 -4px; + color: var(--accent); + font-size: clamp(72px, 20vw, 132px); + font-weight: 250; + line-height: .92; + letter-spacing: -.075em; +} + +.error-card h1 { + margin-bottom: 10px; +} + +.error-card .lead { + margin-bottom: 0; +} + +.error-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 28px; +} + +.button-link { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 42px; + padding: 11px 15px; + border-radius: 13px; + text-decoration: none; + transition: transform .15s ease, background .15s ease, opacity .15s ease; +} + +.button-link:active { + transform: scale(.97); +} + +.button-link.primary { + background: var(--accent); + color: var(--accent-text); + font-weight: 800; +} + +.button-link.primary:hover { + background: var(--accent-strong); +} + +/* Explicit control-source states */ +.list-card.group-controlled { + border-color: var(--group-control-color); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--group-control-color) 30%, transparent); +} + +.group-control-badge, +.group-control-tag { + display: inline-flex; + align-items: center; + width: max-content; + max-width: 100%; + padding: 4px 7px; + border-radius: 999px; + color: var(--group-control-color); + background: color-mix(in srgb, var(--group-control-color) 12%, var(--surface)); + font-size: 10px; + font-weight: 800; + line-height: 1.1; +} + +.group-control-tag { + margin-left: 5px; + vertical-align: middle; +} + +.group-preset-row button.editing:not(.active) { + border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); + color: var(--accent); + background: color-mix(in srgb, var(--accent) 8%, var(--surface)); +} + +.zone-thermostat.lockout-waiting:not(.group-controlled), +.zone-config-card.lockout-waiting:not(.group-controlled) { + border-color: color-mix(in srgb, var(--warning) 58%, var(--line)); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning) 16%, transparent); +} + +.zone-thermostat.lockout-waiting, +.zone-config-card.lockout-waiting { + background-image: linear-gradient(color-mix(in srgb, var(--warning-soft) 42%, transparent), color-mix(in srgb, var(--warning-soft) 42%, transparent)); +} + +/* v0.8.20: group ownership and pending thermostat state clarity. */ +.group-custom-temperature-row { + grid-template-columns: minmax(0, 1fr) auto auto auto; +} + +.group-custom-draft-note { + display: block; + margin-top: 6px; + color: var(--warning); + font-size: 10px; + line-height: 1.35; +} + +.zone-thermostat.lockout-waiting, +.zone-config-card.lockout-waiting { + background-image: linear-gradient(color-mix(in srgb, var(--warning-soft) 48%, transparent), color-mix(in srgb, var(--warning-soft) 48%, transparent)); +} + +.zone-thermostat.group-controlled.lockout-waiting, +.zone-config-card.group-controlled.lockout-waiting { + border-color: var(--group-control-color); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--group-control-color) 30%, transparent), inset 0 0 0 2px color-mix(in srgb, var(--warning) 18%, transparent); +} + +@media (max-width:520px) { + .group-custom-temperature-row { + grid-template-columns: minmax(0, 1fr) auto; + } + + .group-custom-temperature-row input { + grid-column: 1; + } + + .group-custom-temperature-row>span { + grid-column: 2; + } + + .group-custom-temperature-row button { + grid-column: auto; + } +} + +/* Idle groups keep the card neutral; only the state badge carries the group color. */ +.list-card.group-card.group-linked, +.list-card.group-card.group-off { + border-style: solid; + border-color: var(--line); + box-shadow: none; +} + +.group-on-badge, +.group-off-badge { + color: var(--group-control-color); + border: 1px solid color-mix(in srgb, var(--group-control-color) 58%, var(--line)); + background: color-mix(in srgb, var(--group-control-color) 7%, var(--surface)); +} + +.group-off-badge { + border-style: dashed; +} + +/* Color the whole group card only while the group is the active control source. */ +.list-card.group-card.group-linked.group-controlled { + border-style: solid; + border-color: var(--group-control-color); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--group-control-color) 30%, transparent); +} + +/* v0.9.0: visible/cancellable compressor-protection queue. */ +.compressor-queue-panel { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 10px; + padding: 10px 12px; + border: 1px dashed var(--warning, #b7791f); + border-radius: 12px; + background: color-mix(in srgb, var(--warning, #b7791f) 9%, transparent); +} + +.compressor-queue-panel>div { + display: grid; + gap: 2px; + min-width: 0; +} + +.compressor-queue-panel strong { + font-size: .88rem; +} + +.compressor-queue-panel span, +.compressor-queue-panel small { + overflow-wrap: anywhere; +} + +.compressor-queue-panel button { + flex: 0 0 auto; +} + + +/* v0.9.1: global compressor queue overview. */ +.queue-overview-button { + display: inline-flex; + align-items: center; + gap: 7px; +} + +.queue-overview-count { + display: inline-grid; + place-items: center; + min-width: 23px; + height: 23px; + padding: 0 6px; + border-radius: 999px; + background: color-mix(in srgb, var(--text) 9%, transparent); + font-size: .78rem; +} + +.queue-overview-button.has-items { + border-color: var(--warning, #b7791f); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning, #b7791f) 22%, transparent); +} + +.queue-overview-button.has-items .queue-overview-count { + background: color-mix(in srgb, var(--warning, #b7791f) 18%, transparent); + color: var(--text); +} + +.compressor-queue-dialog { + width: min(780px, calc(100% - 24px)); +} + +.compressor-queue-dialog-body { + gap: 16px; +} + +.compressor-queue-summary { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 13px; + border: 1px solid var(--line); + border-radius: 14px; + background: var(--surface); +} + +.compressor-queue-summary span { + color: var(--muted); + font-size: .86rem; +} + +.compressor-queue-summary strong { + font-size: 1.15rem; +} + +.compressor-queue-list { + display: grid; + gap: 10px; +} + +.compressor-queue-item { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + padding: 14px; + border: 1px solid var(--line); + border-radius: 16px; + background: linear-gradient(135deg, color-mix(in srgb, var(--warning, #b7791f) 7%, var(--surface)), var(--surface)); +} + +.compressor-queue-index { + display: grid; + place-items: center; + width: 32px; + height: 32px; + border-radius: 10px; + background: color-mix(in srgb, var(--warning, #b7791f) 15%, transparent); + font-weight: 800; +} + +.compressor-queue-item-main { + min-width: 0; + display: grid; + gap: 8px; +} + +.compressor-queue-item-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.compressor-queue-item-head h3 { + margin: 2px 0 0; + font-size: 1rem; +} + +.compressor-queue-reason { + flex: 0 0 auto; + padding: 5px 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--warning, #b7791f) 14%, transparent); + font-size: .73rem; + font-weight: 700; +} + +.compressor-queue-command { + margin: 0; + font-weight: 700; + overflow-wrap: anywhere; +} + +.compressor-queue-meta { + display: flex; + flex-wrap: wrap; + gap: 6px 12px; + color: var(--muted); + font-size: .78rem; +} + +.compressor-queue-meta strong { + color: var(--text); + font-weight: 700; +} + +.compressor-queue-cancel { + align-self: center; +} + +.compressor-queue-empty { + display: grid; + justify-items: center; + gap: 7px; + padding: 30px 18px; + text-align: center; + border: 1px dashed var(--line); + border-radius: 16px; + color: var(--muted); +} + +.compressor-queue-empty>span { + display: grid; + place-items: center; + width: 42px; + height: 42px; + border-radius: 999px; + background: var(--surface); + font-size: 1.35rem; + color: var(--text); +} + +.compressor-queue-empty strong { + color: var(--text); + font-size: 1rem; +} + +.compressor-queue-empty p { + margin: 0; + max-width: 440px; +} + +.compressor-queue-actions { + position: sticky; + bottom: 0; + padding-top: 10px; + background: linear-gradient(transparent, var(--dialog-bg) 28%); +} + +@media (max-width:620px) { + .compressor-queue-item { + grid-template-columns: auto minmax(0, 1fr); + } + + .compressor-queue-cancel { + grid-column: 1 / -1; + width: 100%; + } + + .compressor-queue-item-head { + display: grid; + } + + .compressor-queue-reason { + justify-self: start; + } +} +/* Visual Flow editor */ +.flow-info-panel { display:flex; align-items:center; justify-content:space-between; gap:20px; } +.flow-info-panel p { margin:.35rem 0 0; color:var(--muted); max-width:780px; } +.flow-card .card-footer small { max-width:70%; } +body.flow-editor-open { overflow:hidden; } +.flow-editor { position:fixed; inset:0; z-index:80; background:var(--bg); display:flex; flex-direction:column; } +.flow-editor[hidden] { display:none !important; } +.flow-editor-bar { min-height:76px; display:flex; align-items:center; justify-content:space-between; gap:20px; padding:10px 18px; border-bottom:1px solid var(--line); background:var(--surface); } +.flow-editor-title { display:flex; align-items:center; gap:12px; min-width:0; } +.flow-editor-title > div { display:grid; gap:2px; min-width:0; } +.flow-name-wrap { min-width:280px; min-height:36px; display:flex; align-items:center; gap:8px; } +.flow-name-view { display:flex; align-items:center; gap:6px; min-width:0; } +.flow-name-view[hidden] { display:none !important; } +.flow-name-text { appearance:none; border:0; background:transparent; color:var(--text); font:inherit; font-weight:750; font-size:1.12rem; line-height:1.25; padding:4px 2px; min-width:0; max-width:min(52vw,620px); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:text; text-align:left; } +.flow-name-text:hover { color:var(--accent); } +.flow-name-edit { appearance:none; border:0; background:transparent; color:var(--muted); width:28px; height:28px; border-radius:7px; padding:0; cursor:pointer; display:grid; place-items:center; opacity:.62; transition:opacity .15s ease,background .15s ease,color .15s ease; } +.flow-name-view:hover .flow-name-edit,.flow-name-edit:focus-visible { opacity:1; color:var(--accent); background:var(--accent-soft); outline:none; } +.flow-editor-title input { border:1px solid var(--line-strong); background:var(--input-bg); color:var(--text); font-weight:750; font-size:1.12rem; padding:7px 10px; min-width:280px; outline:none; border-radius:9px; box-shadow:inset 0 0 0 1px transparent; transition:border-color .15s ease,box-shadow .15s ease,background .15s ease; } +.flow-editor-title input[hidden] { display:none !important; } +.flow-editor-title input:hover { border-color:var(--accent-border); background:var(--surface-2); } +.flow-editor-title input:focus { border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); background:var(--input-bg); } +.flow-draft-badge { border-color:var(--orange); background:color-mix(in srgb,var(--orange) 14%,var(--surface)); color:var(--text); } +.flow-card.is-draft { border-style:dashed; } +.flow-editor-actions { display:flex; align-items:center; gap:10px; } +.flow-enabled { display:flex; align-items:center; gap:8px; color:var(--muted); } +.flow-editor-body { flex:1; min-height:0; display:grid; grid-template-columns:220px minmax(420px,1fr) 300px; } +.flow-palette,.flow-inspector { background:var(--surface); overflow:auto; padding:16px; } +.flow-palette { border-right:1px solid var(--line); } +.flow-inspector { border-left:1px solid var(--line); } +.flow-palette-head { display:grid; gap:4px; margin-bottom:18px; } +.flow-palette-head small { color:var(--muted); line-height:1.35; } +.flow-palette-group { display:grid; gap:7px; margin:0 0 18px; } +.flow-palette-group > span { color:var(--muted); font-size:.72rem; font-weight:750; text-transform:uppercase; letter-spacing:.08em; margin-bottom:2px; } +.flow-palette-group button { text-align:left; border:1px solid var(--line); background:var(--surface-2); color:var(--text); border-radius:10px; padding:9px 10px; } +.flow-palette-group button:hover { border-color:var(--accent-border); background:var(--accent-soft); } +.flow-workspace-wrap { min-width:0; min-height:0; display:grid; grid-template-rows:auto minmax(0,1fr) auto; background:var(--canvas); } +.flow-workspace { position:relative; min-height:0; overflow:auto; outline:none; background-color:var(--canvas); background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px); background-size:24px 24px; } +.flow-nodes { position:relative; width:2400px; height:1500px; } +.flow-edges { position:absolute; inset:0; width:2400px; height:1500px; overflow:visible; pointer-events:none; z-index:1; } +.flow-edges path { fill:none; stroke:var(--muted-2); stroke-width:2.5; pointer-events:stroke; cursor:pointer; } +.flow-edges path:hover { stroke:var(--danger); stroke-width:4; } +.flow-editor .flow-node { position:absolute; z-index:2; display:block; align-content:normal; gap:0; width:170px; min-height:78px; padding:0; border:1px solid var(--line-strong); border-radius:12px; background:var(--surface); box-shadow:0 10px 28px rgba(0,0,0,.18); user-select:none; } +.flow-editor .flow-node.selected { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-soft),0 12px 30px rgba(0,0,0,.22); } +.flow-editor .flow-node-time { border-top:3px solid var(--blue); } +.flow-editor .flow-node-sensor { border-top:3px solid var(--teal); } +.flow-editor .flow-node-logic { border-top:3px solid var(--purple); } +.flow-editor .flow-node-action { border-top:3px solid var(--orange); } +.flow-editor .flow-node-head { display:flex; justify-content:space-between; gap:8px; align-items:center; padding:8px 9px 6px; font-size:.82rem; font-weight:750; cursor:grab; } +.flow-editor .flow-node-head button { border:0; background:transparent; color:var(--muted); padding:0 2px; font-size:1rem; } +.flow-editor .flow-node-body { padding:5px 10px 10px; color:var(--muted); font-size:.76rem; line-height:1.35; overflow-wrap:anywhere; } +.flow-editor .flow-port { position:absolute; top:50%; width:14px; height:14px; margin-top:-7px; border-radius:50%; border:2px solid var(--surface); background:var(--muted-2); padding:0; z-index:4; } +.flow-editor .flow-port-in { left:-8px; } +.flow-editor .flow-port-out { right:-8px; } +.flow-editor .flow-port:hover,.flow-editor .flow-port.armed { background:var(--accent); transform:scale(1.15); } +.flow-empty-hint { position:absolute; z-index:0; left:50%; top:42%; transform:translate(-50%,-50%); text-align:center; display:grid; gap:5px; color:var(--muted); pointer-events:none; } +.flow-natural-preview { display:flex; justify-content:space-between; gap:24px; align-items:center; min-height:58px; padding:10px 18px; border-top:1px solid var(--line); background:var(--surface); } +.flow-preview-item { display:grid; grid-template-columns:auto minmax(0,1fr); gap:10px; align-items:baseline; min-width:0; } +.flow-preview-interpretation { flex:1 1 auto; } +.flow-preview-runtime { flex:0 0 auto; justify-content:end; text-align:right; } +.flow-natural-preview span { color:var(--muted); font-size:.86rem; line-height:1.4; } +.flow-preview-runtime span { white-space:nowrap; } +.flow-inspector-head { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; margin-bottom:18px; } +.flow-inspector-head h3 { margin:2px 0 0; } +.flow-inspector label { display:grid; gap:6px; margin-bottom:13px; color:var(--muted); font-size:.82rem; } +.flow-inspector input,.flow-inspector select { width:100%; border:1px solid var(--line); border-radius:10px; background:var(--input-bg); color:var(--text); padding:9px 10px; } +.flow-inspector .two { display:grid; grid-template-columns:1fr 1fr; gap:10px; } +.flow-day-grid { display:grid; grid-template-columns:1fr 1fr; gap:7px; } +.flow-day-grid label { display:flex; align-items:center; gap:7px; padding:8px; border:1px solid var(--line); border-radius:9px; margin:0; } +.flow-day-grid input { width:auto; } +.flow-inspector-meta { display:grid; gap:5px; margin-top:20px; padding-top:14px; border-top:1px solid var(--line); color:var(--muted); } +.flow-inspector-meta code { font-size:.72rem; overflow-wrap:anywhere; } + +@media (max-width: 980px) { + .flow-editor-body { grid-template-columns:180px minmax(360px,1fr) 260px; } + .flow-name-wrap,.flow-editor-title input { min-width:180px; } +} +@media (max-width: 760px) { + .flow-editor-bar { align-items:flex-start; flex-direction:column; } + .flow-editor-actions { width:100%; justify-content:flex-end; } + .flow-editor-body { grid-template-columns:132px minmax(500px,1fr); } + .flow-inspector { position:absolute; right:0; top:138px; bottom:0; width:min(310px,86vw); z-index:8; box-shadow:-12px 0 30px rgba(0,0,0,.28); } + .flow-palette { padding:10px; } + .flow-palette-group button { font-size:.76rem; padding:8px; } + .flow-natural-preview { flex-direction:column; align-items:stretch; gap:6px; } + .flow-preview-item { grid-template-columns:1fr; gap:2px; } + .flow-preview-runtime { justify-content:start; text-align:left; } + .flow-preview-runtime span { white-space:normal; } +} + +/* Flow diagnostics, templates and portable Flow files */ +.flow-template-card { display:grid; gap:7px; text-align:left; align-content:start; min-height:118px; border:1px solid var(--line); border-radius:14px; background:var(--surface-2); color:var(--text); padding:14px; } +.flow-template-card:hover { border-color:var(--accent); } +.flow-template-card span { color:var(--muted); font-size:.86rem; line-height:1.4; } +.flow-simulation-controls { display:grid; gap:14px; padding:4px 0 16px; border-bottom:1px solid var(--line); } +.flow-simulation-overrides { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:10px; margin-top:8px; } +.flow-test-results { display:grid; gap:10px; margin-top:16px; } +.flow-test-summary,.flow-test-action,.flow-log-row { border:1px solid var(--line); border-radius:12px; background:var(--surface-2); padding:12px; } +.flow-test-summary { display:flex; justify-content:space-between; gap:12px; align-items:center; } +.flow-test-action > div:first-child,.flow-log-row > div:first-child { display:flex; justify-content:space-between; gap:10px; align-items:center; } +.flow-test-action.pass { border-left:4px solid var(--teal); } +.flow-test-action.blocked { border-left:4px solid var(--line-strong); } +.flow-trace { display:grid; gap:5px; margin-top:10px; } +.flow-trace > div { display:flex; justify-content:space-between; gap:10px; padding-top:5px; border-top:1px solid var(--line); font-size:.82rem; } +.flow-trace code { color:var(--muted); max-width:48%; overflow-wrap:anywhere; text-align:right; } +.flow-log-row p { margin:8px 0 4px; } +.flow-log-row small { color:var(--muted); } +@media (max-width:760px) { .flow-test-summary { align-items:flex-start; flex-direction:column; } } +.flow-editor-actions { flex-wrap:wrap; justify-content:flex-end; } +.flow-editor-actions .secondary { padding-inline:10px; } +@media (max-width:1100px) { + .flow-editor-bar { align-items:flex-start; } + .flow-editor-actions { max-width:70%; } +} +@media (max-width:760px) { + .flow-editor-actions { max-width:none; justify-content:flex-start; } +} +.flow-device-options { margin:10px 0; padding:10px 12px; border:1px solid var(--line); border-radius:12px; background:var(--surface-2); } +.flow-device-options summary { cursor:pointer; font-weight:650; } +.flow-device-options[open] summary { margin-bottom:10px; } + +/* Flow list, multi-select and reusable inputs */ +.flow-page-actions { display:flex; align-items:center; gap:10px; } +.flow-list-section { margin-top:22px; padding-top:20px; border-top:1px solid var(--line); } +.flow-list-heading { display:flex; align-items:flex-end; justify-content:space-between; gap:16px; margin-bottom:14px; } +.flow-list-heading h2 { margin:3px 0 0; } +.flow-list-heading-count { justify-content:flex-end; } +.flow-card.is-disabled { opacity:.72; } +.flow-card.is-disabled:hover { opacity:.9; } +.flow-workspace-toolbar { min-height:44px; display:flex; align-items:center; gap:7px; padding:6px 10px; border-bottom:1px solid var(--line); background:var(--surface); } +.flow-workspace-toolbar .secondary { padding:7px 9px; font-size:.78rem; } +.flow-workspace-toolbar .badge { margin-left:2px; } +.flow-shared-input-list { display:grid; gap:10px; } +.flow-shared-input-row { display:flex; align-items:center; justify-content:space-between; gap:14px; padding:12px 14px; border:1px solid var(--line); border-radius:12px; background:var(--surface-2); } +.flow-shared-input-row > div:first-child { min-width:0; display:grid; gap:4px; } +.flow-shared-input-row small,.flow-shared-input-row code { color:var(--muted); overflow-wrap:anywhere; } +.flow-shared-input-actions { display:flex; align-items:center; gap:8px; flex-shrink:0; } +.flow-shared-input-fields { display:grid; gap:12px; } +.flow-shared-input-fields .two { display:grid; grid-template-columns:1fr 1fr; gap:10px; } +.flow-template-list { display:grid; grid-template-columns:1fr; gap:22px; } +.flow-template-category-head { display:grid; gap:3px; } +.flow-template-category-head span { color:var(--muted); font-size:.84rem; } +.flow-template-category-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:12px; } +@media (max-width:760px) { + .flow-list-heading { align-items:flex-start; } + .flow-shared-input-row { align-items:flex-start; flex-direction:column; } + .flow-shared-input-actions { width:100%; } + .flow-shared-input-actions button { flex:1; } + .flow-shared-input-fields .two { grid-template-columns:1fr; } +} + +/* v0.9.9: Flow shared-input navigation and preset category tabs */ +.flow-shared-settings-head { justify-content:space-between; align-items:flex-start; } +.flow-shared-settings-head > div { min-width:0; } +.flow-shared-add-button { flex:0 0 auto; align-self:flex-start; margin-left:auto; } +.flow-shared-settings-link { margin-top:12px; width:max-content; max-width:100%; } +.flow-shared-settings.is-linked-target { outline:2px solid var(--accent); outline-offset:3px; } +.flow-template-tabs { display:flex; gap:8px; overflow-x:auto; padding:4px 0 10px; margin-top:14px; border-bottom:1px solid var(--line); scrollbar-width:thin; } +.flow-template-tab { flex:0 0 auto; border:1px solid var(--line); border-radius:999px; background:var(--surface-2); color:var(--muted); padding:8px 12px; white-space:nowrap; } +.flow-template-tab.active { border-color:var(--accent-border); background:var(--accent-soft); color:var(--accent); } +.flow-template-list { margin-top:14px; } +.flow-template-category-single { display:grid; gap:12px; } +.flow-template-category-single .flow-template-category-head { padding-bottom:2px; } +@media (max-width:760px) { + .flow-shared-settings-head { gap:12px; } + .flow-shared-add-button { width:auto; } +} + +/* v0.9.10: Flow preset library preview, favorites/search and shared-input diagnostics */ +.flow-shared-usage { display:grid; gap:5px; margin-top:5px; } +.flow-shared-usage > div { display:flex; flex-wrap:wrap; align-items:center; gap:6px; } +.flow-shared-usage .link-button { border:0; padding:0; background:transparent; color:var(--accent); text-align:left; text-decoration:underline; text-underline-offset:2px; cursor:pointer; font:inherit; font-size:.78rem; } +.flow-shared-unused { margin-top:4px; } +.flow-shared-test-panel { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px 14px; align-items:start; padding:12px; border:1px solid var(--line); border-radius:12px; background:var(--surface-2); } +.flow-shared-test-panel .field-note { margin:3px 0 0; } +.flow-shared-test-result { grid-column:1 / -1; display:grid; gap:8px; padding:10px 12px; border:1px solid var(--line); border-radius:10px; background:var(--surface); } +.flow-shared-test-result.pass { border-left:4px solid var(--teal); } +.flow-shared-test-result.fail { border-left:4px solid var(--line-strong); } +.flow-shared-test-result > div:first-child { display:flex; justify-content:space-between; gap:10px; align-items:center; } +.flow-shared-test-result dl { display:grid; gap:5px; margin:0; } +.flow-shared-test-result dl > div { display:grid; grid-template-columns:120px minmax(0,1fr); gap:8px; } +.flow-shared-test-result dt { color:var(--muted); } +.flow-shared-test-result dd { margin:0; min-width:0; overflow-wrap:anywhere; } +.flow-shared-test-result > small { color:var(--muted); } + +#flowTemplateDialog { width:min(1180px,calc(100vw - 32px)); } +.flow-template-toolbar { margin-top:14px; } +.flow-template-toolbar label { display:grid; gap:6px; } +.flow-template-toolbar input { width:100%; border:1px solid var(--line); border-radius:10px; background:var(--input-bg); color:var(--text); padding:10px 12px; } +.flow-template-browser { display:grid; grid-template-columns:minmax(0,1.15fr) minmax(320px,.85fr); gap:18px; align-items:start; } +.flow-template-catalog { min-width:0; } +.flow-template-preview { position:sticky; top:0; display:grid; gap:12px; margin-top:14px; padding:14px; border:1px solid var(--line); border-radius:14px; background:var(--surface-2); min-height:250px; } +.flow-template-preview-head { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; } +.flow-template-preview-head h3 { margin:3px 0 0; } +.flow-template-preview > p { margin:0; color:var(--muted); line-height:1.45; } +.flow-template-preview-stats { display:flex; flex-wrap:wrap; gap:7px; } +.flow-template-preview-stats span { padding:5px 8px; border:1px solid var(--line); border-radius:999px; color:var(--muted); font-size:.78rem; } +.flow-template-preview-canvas { position:relative; height:250px; overflow:hidden; border:1px solid var(--line); border-radius:12px; background:var(--surface); } +.flow-template-preview-canvas svg { position:absolute; inset:0; width:100%; height:100%; pointer-events:none; } +.flow-template-preview-canvas line { stroke:var(--line-strong); stroke-width:.7; vector-effect:non-scaling-stroke; } +.flow-template-preview-node { position:absolute; transform:translate(-50%,-50%); width:104px; max-width:28%; padding:6px 7px; border:1px solid var(--line); border-radius:8px; background:var(--surface-2); box-shadow:0 3px 10px rgba(0,0,0,.12); font-size:.68rem; line-height:1.2; text-align:center; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.flow-template-preview-node-sensor { border-top:3px solid var(--blue, var(--accent)); } +.flow-template-preview-node-logic { border-top:3px solid var(--accent); } +.flow-template-preview-node-action { border-top:3px solid var(--teal); } +.flow-template-requirements { display:grid; gap:8px; } +.flow-template-requirements > div { display:flex; flex-wrap:wrap; gap:6px; } +.flow-template-requirement { padding:5px 8px; border:1px solid var(--line); border-radius:999px; font-size:.75rem; } +.flow-template-requirement.ok { background:var(--surface); } +.flow-template-requirement.missing { border-color:var(--danger, var(--line-strong)); } +.flow-template-requirement.info { color:var(--muted); } +.flow-template-card { position:relative; display:grid; min-height:0; padding:0; overflow:hidden; } +.flow-template-card.selected { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent-border); } +.flow-template-card-main { display:grid; gap:7px; width:100%; min-height:118px; border:0; background:transparent; color:var(--text); padding:14px 42px 14px 14px; text-align:left; align-content:start; cursor:pointer; } +.flow-template-card-main span { color:var(--muted); font-size:.86rem; line-height:1.4; } +.flow-template-favorite { width:26px; height:26px; display:grid; place-items:center; border:0; padding:0; background:transparent; color:var(--muted); cursor:pointer; font-size:1.22rem; line-height:1; opacity:.72; transition:color .15s ease, opacity .15s ease, transform .15s ease; } +.flow-template-card > .flow-template-favorite { position:absolute; right:8px; top:8px; } +.flow-template-favorite:hover { color:var(--text); background:transparent; opacity:1; transform:scale(1.08); } +.flow-template-favorite:focus-visible { outline:2px solid var(--accent); outline-offset:2px; opacity:1; } +.flow-template-favorite.active { color:var(--accent); background:transparent; opacity:1; } +.flow-template-favorite.active:hover { background:transparent; } +.flow-template-favorite:active { transform:scale(.92); } +.flow-template-preview .form-actions { margin-top:0; padding-top:10px; border-top:1px solid var(--line); } + +@media (max-width:900px) { + .flow-template-browser { grid-template-columns:1fr; } + .flow-template-preview { position:static; } +} +@media (max-width:760px) { + .flow-shared-test-panel { grid-template-columns:1fr; } + .flow-shared-test-panel > button { width:100%; } + .flow-shared-test-result { grid-column:1; } + .flow-shared-test-result dl > div { grid-template-columns:1fr; gap:2px; } + .flow-template-preview-canvas { height:220px; } +} + +/* Keep zone hysteresis/source controls aligned without stretching selects. */ +.zone-hysteresis-row { align-items:start; } +.flow-editor .flow-node-current { margin-top:6px; padding-top:6px; border-top:1px solid color-mix(in srgb, var(--border) 72%, transparent); display:flex; align-items:baseline; justify-content:space-between; gap:8px; } +.flow-editor .flow-node-current span { color:var(--muted); font-size:.68rem; white-space:nowrap; } +.flow-editor .flow-node-current strong { min-width:0; max-width:72px; color:var(--text); font-size:.76rem; font-weight:700; text-align:right; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + +/* Compact, category-coded Flow palette */ +.flow-palette-group { padding-left:8px; border-left:3px solid var(--line-strong); } +.flow-palette-group button { min-height:30px; padding:6px 8px; border-radius:8px; font-size:.76rem; line-height:1.15; } +.flow-palette-trigger { border-left-color:var(--warning); } +.flow-palette-time { border-left-color:var(--blue); } +.flow-palette-timeop { border-left-color:var(--info); } +.flow-palette-sensor { border-left-color:var(--teal); } +.flow-palette-logic { border-left-color:var(--purple); } +.flow-palette-action { border-left-color:var(--orange); } +.flow-palette-haaction { border-left-color:var(--accent); } +.flow-editor .flow-node-trigger { border-top:3px solid var(--warning); } +.flow-editor .flow-node-timeop { border-top:3px solid var(--info); } +.flow-editor .flow-node-haaction { border-top:3px solid var(--accent); } +.flow-template-preview-node-trigger { border-top:3px solid var(--warning); } +.flow-template-preview-node-time, .flow-template-preview-node-timeop { border-top:3px solid var(--blue); } +.flow-template-preview-node-haaction { border-top:3px solid var(--accent); } + + +/* Compact Flow state control and mobile action entry point. */ +.flow-enabled { white-space:nowrap; } +.flow-enabled input { width:16px; min-height:16px; accent-color:var(--accent); } +.flow-mobile-actions-button { display:none; } +.flow-inspector-head-actions { display:flex; align-items:center; gap:7px; } +.flow-inspector-close { display:none; width:34px; height:34px; padding:0; font-size:1.1rem; } + +/* Clarify which HA aliases are metric sources and which are only Flow references. */ +.sensor-alias-entity { min-width:0; display:flex; align-items:center; gap:7px; } +.sensor-alias-entity > .mono { min-width:0; overflow:hidden; color:var(--muted); font-size:11px; text-overflow:ellipsis; white-space:nowrap; } +.sensor-source-badges { display:flex; flex:0 0 auto; gap:4px; } +.sensor-source-badge { display:inline-flex; align-items:center; min-height:20px; padding:2px 6px; border:1px solid var(--line); border-radius:999px; font-size:.64rem; font-weight:750; line-height:1; white-space:nowrap; } +.sensor-source-badge.metrics { border-color:var(--accent-border); background:var(--accent-soft); color:var(--accent); } +.sensor-source-badge.flow { background:var(--surface); color:var(--muted); } + +/* UX refresh: clearer hierarchy, safer destructive actions and touch-first layouts. */ +:root { --radius-small:10px; --radius-medium:14px; } + +.device-card,.panel,.list-card { border-radius:var(--radius); } +button { border-radius:11px; } +.metric { border-radius:14px; } +.lead { max-width:780px; line-height:1.55; } +.section-heading h1 { margin-bottom:0; } + +.automation-tabs { + display:flex; align-items:center; gap:5px; width:max-content; max-width:100%; + margin:-2px 0 14px; padding:5px; overflow-x:auto; border:1px solid var(--line); + border-radius:14px; background:var(--surface); scrollbar-width:none; +} +.automation-tabs::-webkit-scrollbar { display:none; } +.automation-tabs button { flex:0 0 auto; min-height:38px; padding:8px 12px; color:var(--muted); background:transparent; font-size:12px; font-weight:750; } +.automation-tabs button.active { color:var(--accent); background:var(--accent-soft); box-shadow:inset 0 0 0 1px var(--accent-border); } + +.mobile-nav-only { display:block; } +.desktop-nav-only { display:none; } +.nav-section-label,.menu-section-label { color:var(--muted); font-size:9px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; } +.menu-section-label { display:block; padding:14px 9px 5px; } +.more-menu-list .menu-section-label:first-child { padding-top:5px; } + +.card-menu { align-items:center; } +.card-primary-action { font-weight:700; } +.card-overflow { position:relative; flex:0 0 auto; } +.card-overflow summary { + display:grid; place-items:center; width:38px; height:38px; border:1px solid var(--line); + border-radius:10px; color:var(--muted); background:var(--surface-muted); cursor:pointer; + list-style:none; font-size:16px; font-weight:800; letter-spacing:.08em; +} +.card-overflow summary::-webkit-details-marker { display:none; } +.card-overflow[open] summary { color:var(--text); border-color:var(--line-strong); background:var(--surface-2); } +.card-overflow-menu { + position:absolute; z-index:25; right:0; bottom:calc(100% + 7px); min-width:150px; padding:6px; + border:1px solid var(--line); border-radius:12px; background:var(--surface); box-shadow:0 14px 34px rgba(0,0,0,.28); +} +.card-overflow-menu button { width:100%; text-align:left; } +.card-overflow-menu .danger { background:var(--danger-soft); } + +.flow-save-status { display:inline-flex; align-items:center; min-height:24px; color:var(--muted); font-size:.72rem; font-weight:750; white-space:nowrap; } +.flow-save-status::before { content:""; width:6px; height:6px; margin-right:6px; border-radius:50%; background:var(--accent); } +.flow-save-status.is-dirty { color:var(--warning); } +.flow-save-status.is-dirty::before { background:var(--warning); } +.flow-canvas { position:relative; width:2400px; height:1500px; transform-origin:0 0; } +.flow-canvas .flow-nodes,.flow-canvas .flow-edges { width:2400px; height:1500px; } +.flow-workspace-toolbar { justify-content:space-between; } +.flow-workspace-primary-actions,.flow-zoom-controls { display:flex; align-items:center; gap:7px; min-width:0; } +.flow-add-block-button { display:none; align-items:center; justify-content:center; gap:5px; } +.flow-zoom-controls .icon-button { width:32px; height:32px; min-height:32px; padding:0; font-size:17px; } +.flow-zoom-controls > span { min-width:44px; color:var(--muted); font-size:.72rem; font-weight:800; text-align:center; font-variant-numeric:tabular-nums; } +.flow-fit-button { min-height:32px; padding:6px 9px; font-size:.75rem; } +.flow-inspector-expand { display:none; width:34px; height:34px; padding:0; } + +.flow-block-dialog { width:min(620px,calc(100% - 24px)); } +.flow-block-dialog .dialog-head { padding:16px 16px 8px; } +.flow-block-search { padding:4px 16px 10px; } +.flow-block-library { display:grid; gap:14px; padding:0 16px 18px; } +.flow-block-library-group { padding-left:10px; border-left:3px solid var(--line-strong); } +.flow-block-library-group.flow-palette-trigger { border-left-color:var(--warning); } +.flow-block-library-group.flow-palette-time { border-left-color:var(--blue); } +.flow-block-library-group.flow-palette-timeop { border-left-color:var(--info); } +.flow-block-library-group.flow-palette-sensor { border-left-color:var(--teal); } +.flow-block-library-group.flow-palette-logic { border-left-color:var(--purple); } +.flow-block-library-group.flow-palette-action { border-left-color:var(--orange); } +.flow-block-library-group.flow-palette-haaction { border-left-color:var(--accent); } +.flow-block-library-group h3 { margin:0 0 7px; color:var(--muted); font-size:.72rem; letter-spacing:.07em; text-transform:uppercase; } +.flow-block-library-group > div { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; } +.flow-block-library-group button { min-height:42px; text-align:left; } + +/* Port stays visually small but gets a comfortable touch target. */ +.flow-editor .flow-port { width:16px; height:16px; margin-top:-8px; } +.flow-editor .flow-port-in { left:-9px; } +.flow-editor .flow-port-out { right:-9px; } +.flow-editor .flow-port::after { content:""; position:absolute; inset:-11px; border-radius:50%; } + +.flow-preview-toggle { display:none; } +.flow-preview-details { display:flex; justify-content:space-between; gap:24px; align-items:center; width:100%; min-width:0; } + +@media (min-width:900px) { + .mobile-nav-only { display:none !important; } + .desktop-nav-only { display:flex; } + .bottom-nav { + top:calc(var(--topbar-height,72px) + 14px); bottom:14px; left:max(14px,calc((100vw - 1440px)/2)); + display:flex; flex-direction:column; gap:2px; width:190px; max-height:calc(100dvh - var(--topbar-height,72px) - 28px); + padding:10px; overflow-y:auto; border-radius:16px; scrollbar-width:thin; + } + .bottom-nav .nav-section-label { padding:10px 9px 4px; } + .bottom-nav .nav-section-label:first-child { padding-top:3px; } + .bottom-nav button { display:flex; align-items:center; justify-content:flex-start; gap:10px; min-height:39px; padding:8px 10px; border-radius:10px; text-align:left; } + .bottom-nav button span { display:grid; place-items:center; flex:0 0 24px; width:24px; font-size:17px; } + .bottom-nav button b { min-width:0; overflow:hidden; font-size:11px; text-overflow:ellipsis; white-space:nowrap; } + .bottom-nav button.active { background:var(--accent-soft); } + main { width:min(1440px,100%); padding-left:222px; padding-right:24px; } +} + +@media (max-width:760px) { + main { padding:18px 12px calc(94px + env(safe-area-inset-bottom)); } + .desktop-nav-only { display:none !important; } + .mobile-nav-only { display:grid; } + .bottom-nav { grid-template-columns:repeat(5,minmax(0,1fr)); } + .bottom-nav button { min-height:48px; } + .bottom-nav button span { font-size:19px; } + + .section-heading { gap:12px; margin:4px 0 14px; } + .section-heading h1 { font-size:26px; } + .section-heading > button,.section-heading > .section-actions,.section-heading > .section-heading-actions { width:100%; } + .section-heading > button { min-height:44px; } + .flow-page-actions,.section-heading-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; } + .flow-page-actions button,.section-heading-actions button { width:100%; min-height:44px; } + .automation-tabs { width:100%; margin-bottom:12px; scroll-snap-type:x proximity; } + .automation-tabs button { flex:1 0 auto; scroll-snap-align:start; } + + .device-card,.panel,.list-card { border-radius:14px; } + .device-card { padding:15px; } + .card-footer { gap:9px; } + .card-menu { width:100%; justify-content:flex-end; } + .card-menu > button { min-height:40px; } + .card-primary-action { flex:1 1 auto; } + .card-overflow summary { width:40px; height:40px; } + + dialog:not(.auth-dialog) { + inset:auto 0 0; width:100%; max-width:none; max-height:88dvh; margin:0; + border-right:0; border-bottom:0; border-left:0; border-radius:18px 18px 0 0; + overscroll-behavior:contain; + } + dialog:not(.auth-dialog) .dialog-head { position:sticky; top:0; z-index:5; padding-top:14px; background:var(--dialog-bg); } + dialog:not(.auth-dialog) .dialog-head::before { content:""; position:absolute; top:6px; left:50%; width:38px; height:4px; border-radius:999px; background:var(--line-strong); transform:translateX(-50%); } + .dialog-form { padding:14px 14px calc(14px + env(safe-area-inset-bottom)); } + .dialog-form .form-actions { position:sticky; bottom:0; z-index:4; margin:6px -14px -14px; padding:10px 14px calc(10px + env(safe-area-inset-bottom)); border-top:1px solid var(--line); background:color-mix(in srgb,var(--dialog-bg) 96%,transparent); backdrop-filter:blur(12px); } + .dialog-form .form-actions > button { flex:1 1 0; min-height:44px; } + + .history-tabs { margin-inline:-12px; padding-inline:12px; scroll-padding-inline:12px; scroll-snap-type:x proximity; } + .history-tabs button { min-height:40px; scroll-snap-align:start; } + .history-toolbar-panel { padding:12px; } + + .settings-mode-tabs { position:sticky; top:calc(var(--topbar-height,64px) + 6px); z-index:8; margin-inline:-4px; background:color-mix(in srgb,var(--bg) 94%,transparent); backdrop-filter:blur(12px); } + .settings-save-bar { bottom:calc(74px + env(safe-area-inset-bottom)); margin-inline:-4px; padding:10px; } + .settings-save-bar span { display:none; } + .settings-save-bar button { width:100%; min-height:44px; } + + /* Mobile Flow: compact header, canvas-first editor and bottom-sheet inspector. */ + .flow-editor-bar { + display:grid; grid-template-columns:minmax(0,1fr) auto; grid-template-rows:auto auto; + align-items:center; width:100%; gap:6px 8px; padding:7px 8px; overflow:hidden; + } + .flow-editor-title { grid-column:1; grid-row:1; width:100%; min-width:0; gap:8px; } + .flow-editor-title > div { width:100%; min-width:0; } + .flow-editor-title .eyebrow { display:none; } + .flow-name-wrap { display:flex; width:100%; min-width:0; gap:6px; } + .flow-name-view { min-width:0; flex:1 1 auto; } + .flow-name-text { min-width:0; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .flow-editor-title input { width:100%; min-width:0; flex:1 1 auto; font-size:1rem; } + .flow-editor-bar > .flow-save-button { + grid-column:2; grid-row:1; align-self:stretch; min-width:0; min-height:38px; + padding-inline:11px; white-space:nowrap; + } + .flow-editor-actions { + grid-column:1 / -1; grid-row:2; display:flex; align-items:center; justify-content:space-between; + width:100%; min-width:0; max-width:none; gap:8px; overflow:hidden; + } + .flow-editor-actions .flow-desktop-action,.flow-save-status,.flow-compile-status { display:none; } + .flow-editor-actions .flow-enabled { + display:flex; flex:0 1 auto; width:auto; min-width:0; min-height:34px; + justify-content:flex-start; gap:7px; padding:0 4px; font-size:.78rem; + } + .flow-editor-actions .flow-enabled span { display:inline; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .flow-editor-actions .flow-mobile-actions-button { + display:inline-flex; flex:0 0 auto; width:auto; min-width:96px; min-height:34px; + margin-left:auto; padding:6px 12px; justify-content:center; + } + + .flow-editor-body { position:relative; display:flex; flex:1 1 auto; flex-direction:column; height:0; min-width:0; min-height:0; overflow:hidden; } + .flow-workspace-wrap { + order:1; display:grid; grid-template-rows:auto minmax(0,1fr) auto; flex:1 1 auto; + width:100%; height:100%; min-width:0; min-height:0; overflow:hidden; + } + .flow-workspace-toolbar { display:flex; flex:0 0 auto; width:100%; min-width:0; min-height:48px; padding:6px 8px; gap:7px; justify-content:space-between; overflow:hidden; } + .flow-workspace-primary-actions { display:flex; flex:1 1 auto; min-width:0; overflow:hidden; } + .flow-add-block-button { display:inline-flex; min-width:0; max-width:128px; min-height:36px; padding:7px 10px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .flow-selection-action,#flowSelectionCount { display:none; } + .flow-zoom-controls { display:flex; flex:0 0 auto; margin-left:auto; gap:4px; } + .flow-zoom-controls .icon-button { width:34px; height:34px; min-height:34px; } + .flow-zoom-controls > span { display:none; } + .flow-fit-button { width:34px; min-width:34px; min-height:34px; padding:0; font-size:0; } + .flow-fit-button::before { content:"⊡"; font-size:17px; line-height:1; } + .flow-palette { display:none !important; } + .flow-workspace { height:100%; min-width:0; min-height:0 !important; max-height:none; overflow:auto; overscroll-behavior:contain; touch-action:pan-x pan-y; } + + .flow-natural-preview { display:block; flex:0 0 auto; min-height:0; max-height:none; padding:0; overflow:hidden; } + .flow-preview-toggle { + display:flex; align-items:center; justify-content:space-between; width:100%; min-height:38px; + padding:7px 10px; border:0; border-radius:0; background:var(--surface); color:var(--text); text-align:left; + } + .flow-preview-toggle strong { font-size:.78rem; } + .flow-preview-toggle-icon { color:var(--muted); font-size:1rem; transition:transform .15s ease; } + .flow-preview-details { display:none; flex-direction:column; align-items:stretch; gap:7px; max-height:32dvh; padding:8px 10px 10px; overflow:auto; } + .flow-natural-preview.is-expanded .flow-preview-details { display:flex; } + .flow-natural-preview.is-expanded .flow-preview-toggle { border-bottom:1px solid var(--line); } + .flow-natural-preview.is-expanded .flow-preview-toggle-icon { transform:rotate(180deg); } + .flow-preview-item { gap:1px; } + .flow-natural-preview strong { font-size:.78rem; } + .flow-natural-preview span { font-size:.76rem; } + + .flow-inspector { + display:none; position:fixed; inset:auto 0 0; width:100%; max-height:56dvh; + padding:22px 14px calc(14px + env(safe-area-inset-bottom)); overflow:auto; + border:0; border-top:1px solid var(--line-strong); border-radius:18px 18px 0 0; + z-index:95; box-shadow:0 -14px 34px rgba(0,0,0,.3); transition:max-height .18s ease; + } + .flow-inspector.has-selection { display:block; } + .flow-inspector::before { content:""; position:absolute; top:7px; left:50%; width:38px; height:4px; border-radius:999px; background:var(--line-strong); transform:translateX(-50%); } + .flow-inspector.is-expanded { max-height:90dvh; } + .flow-inspector-close,.flow-inspector-expand { display:inline-grid; place-items:center; } + .flow-inspector-head { position:sticky; top:-22px; z-index:4; margin:-8px -2px 14px; padding:10px 2px 8px; background:var(--surface); } + .flow-inspector-delete { padding-inline:10px; } + .flow-inspector .two { grid-template-columns:1fr; } + .flow-inspector label { font-size:.78rem; } + .flow-inspector input,.flow-inspector select,.flow-inspector textarea { min-height:44px; } + + .flow-block-dialog { max-height:82dvh !important; } + .flow-block-library-group > div { grid-template-columns:1fr; } + .flow-block-library-group button { min-height:46px; } + + .sensor-alias-row { grid-template-columns:minmax(0,1fr) 34px; } + .sensor-alias-entity { grid-column:1 / -1; } + .sensor-alias-row input { grid-column:1; } + .sensor-alias-clear { grid-column:2; } + + /* Do not let long settings/help copy dominate small screens. */ + .settings-block { gap:14px; } + .settings-block-head h3 { font-size:16px; } + .settings-block-head p,.field-note { line-height:1.45; } +} + +@media (max-width:420px) { + .section-heading h1 { font-size:24px; } + .flow-editor-actions .flow-mobile-actions-button { min-width:88px; padding-inline:10px; } +} + +/* Readability and form consistency fixes found during the wider UX pass. */ +textarea { font:inherit; } +textarea { + width:100%; min-height:96px; resize:vertical; border:1px solid var(--line); border-radius:12px; + outline:0; padding:10px 12px; color:var(--text); background:var(--input-bg); line-height:1.45; +} +textarea:focus { border-color:var(--accent); outline:2px solid color-mix(in srgb,var(--accent) 18%,transparent); outline-offset:1px; } +.flow-empty-hint button { pointer-events:auto; margin-top:7px; justify-self:center; } +#controlPlan .plan-card>p { font-size:11.5px; line-height:1.45; } +#controlPlan .plan-events li { font-size:10.5px; line-height:1.4; } +#controlPlan .plan-rule-summary strong { font-size:11px; } +#controlPlan .plan-rule-summary small { font-size:10px; } +#controlPlan .plan-card-head .eyebrow { font-size:9px; } + +@media (prefers-reduced-motion:reduce) { + *,*::before,*::after { scroll-behavior:auto !important; animation-duration:.01ms !important; animation-iteration-count:1 !important; transition-duration:.01ms !important; } +} +.flow-card[data-flow-card-id] { cursor:pointer; } +.flow-card[data-flow-card-id]:focus-visible { outline:2px solid color-mix(in srgb,var(--accent) 55%,transparent); outline-offset:2px; } +.flow-card[data-flow-card-id] button,.flow-card[data-flow-card-id] details { cursor:pointer; } +.flow-generated-card { border-left:3px solid var(--accent-border); } +.flow-generated-card .card-footer > small { color:var(--accent); } + + +/* 0.11.4 production layout guards and in-app chart preview. */ +html,body { max-width:100%; overflow-x:clip; } +main,.view { min-width:0; max-width:100%; } +.flow-info-panel,.flow-info-panel > div { min-width:0; } +.flow-info-panel > .badge { flex:0 1 auto; max-width:100%; white-space:normal; overflow-wrap:anywhere; text-align:center; } + +body.flow-editor-open { width:100%; height:100vh; height:100dvh; overflow:hidden; overscroll-behavior:none; } +.flow-editor { width:100vw; width:100dvw; height:100vh; height:100dvh; max-width:100%; overflow:hidden; } +.flow-editor-bar,.flow-editor-title,.flow-editor-actions,.flow-editor-body,.flow-workspace-wrap,.flow-workspace-toolbar { min-width:0; max-width:100%; } + +.chart-title-row > div:first-child { min-width:0; } +.chart-title-actions { display:flex; align-items:center; gap:7px; flex:0 0 auto; } +.chart-fullscreen-button { min-width:34px; height:32px; padding:0 9px; border:1px solid var(--line); background:var(--surface-muted); color:var(--text-soft); font-size:16px; line-height:1; } +.chart-fullscreen-button:hover { background:var(--surface-2); } +body.chart-fullscreen-open { overflow:hidden; } +body.chart-fullscreen-open::before { content:""; position:fixed; inset:0; z-index:139; background:rgba(0,0,0,.68); backdrop-filter:blur(3px); } +.history-chart-card.chart-fullscreen-fallback { + position:fixed; left:50%; top:50%; z-index:140; + width:min(1200px,calc(100vw - 28px)); height:min(820px,calc(100dvh - 28px)); max-width:none; + margin:0; padding:14px; transform:translate(-50%,-50%); + display:grid; grid-template-rows:auto minmax(0,1fr) auto; gap:8px; overflow:hidden; + border:1px solid var(--line-strong); border-radius:16px; background:var(--surface); + box-shadow:0 28px 90px rgba(0,0,0,.58); +} +.history-chart-card.chart-fullscreen-fallback .chart-title-row { margin:0; } +.history-chart-card.chart-fullscreen-fallback .chart-title-row p, +.history-chart-card.chart-fullscreen-fallback .chart-hover-hint { display:none; } +.history-chart-card.chart-fullscreen-fallback .chart-wrap { height:100%; min-height:0; overflow:auto; border:1px solid var(--line); border-radius:10px; background:var(--surface-muted); } +.history-chart-card.chart-fullscreen-fallback .legend { max-height:88px; margin:0; overflow:auto; } + +@media (max-width:760px) { + body:not(.flow-editor-open) .bottom-nav { position:fixed; right:0; bottom:0; left:0; z-index:60; max-width:100vw; overflow-x:hidden; } + .flow-info-panel { flex-direction:column; align-items:flex-start; gap:12px; padding:16px; overflow:hidden; } + .flow-info-panel > div { width:100%; } + .flow-info-panel > .badge { align-self:flex-start; text-align:left; } + .flow-shared-settings-link { width:100%; max-width:100%; white-space:normal; text-align:left; } + .automation-tabs { width:100%; min-width:0; } + + .chart-title-row { gap:8px; } + .chart-title-actions { width:100%; justify-content:flex-end; } + .chart-fullscreen-button { min-height:36px; height:36px; } + .history-chart-card.chart-fullscreen-fallback { width:calc(100vw - 12px); height:calc(100dvh - 18px); padding:9px; border-radius:13px; } + .history-chart-card.chart-fullscreen-fallback .chart-title-row { flex-direction:row; align-items:center; gap:8px; } + .history-chart-card.chart-fullscreen-fallback .chart-title-row > div:first-child { flex:1 1 auto; min-width:0; } + .history-chart-card.chart-fullscreen-fallback .chart-title-row h3 { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .history-chart-card.chart-fullscreen-fallback .chart-title-actions { width:auto; flex:0 0 auto; } + .history-chart-card.chart-fullscreen-fallback .legend { max-height:70px; } +} + +@media (max-width:560px) { + .chart-title-actions { gap:5px; } + .chart-zoom-controls button { height:36px; } +} diff --git a/mock/theme-init.js b/mock/theme-init.js new file mode 100644 index 0000000..b5bb25f --- /dev/null +++ b/mock/theme-init.js @@ -0,0 +1,9 @@ +'use strict'; +(() => { + const row = document.cookie.split('; ').find(item => item.startsWith('gree_controller_theme=')); + const requested = row ? decodeURIComponent(row.split('=').slice(1).join('=')) : 'system'; + const resolved = requested === 'light' || requested === 'dark' + ? requested + : (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); + document.documentElement.dataset.theme = resolved; +})(); diff --git a/web/index.html b/web/index.html index 0912bbd..447a0c7 100644 --- a/web/index.html +++ b/web/index.html @@ -182,6 +182,11 @@

Flow

+
One place for schedules and automations

Build the logic from blocks. GREE Controller translates Flow into generated schedules and automations. Generated entries are read-only outside this editor.

@@ -200,6 +205,11 @@

Schedules

+

Quick schedule setup

@@ -228,6 +238,11 @@

Automations

+
Use thermostat presets for daily comfort

Schedules and zone presets handle normal heating/cooling automatically. @@ -396,12 +411,12 @@

-

Wspólne wejścia Flow

Zdefiniuj wartości, stany i czujniki raz, a potem używaj ich w wielu Flow. Operator jest opcjonalny.

+

Wspólne wejścia Flow

Zdefiniuj wspólne źródła wartości raz i używaj ich w wielu Flow. Czujniki HA dodane tylko tutaj nie są zapisywane do metryk.

-

Sensor aliases

Friendly names are used in charts and selectors; the original entity_id remains unchanged.

+

Sensor aliases

Badges show whether an entity is collected as metrics, used by Flow, or both.

@@ -694,7 +709,8 @@ + +
Blocks

Add block

+ +
+
+ + +

Actions

+ +
+ @@ -775,22 +811,37 @@

More

-
`; + return `

${esc(title)}

${esc(hint || '')}

${esc(tr('history.hoverHint'))}
`; } function historySeriesColor(index) { return cssColor(HISTORY_COLORS[index % HISTORY_COLORS.length], `hsl(${(index * 67) % 360} 68% 55%)`); } + +document.addEventListener('keydown', event => { + if (event.key === 'Escape') closeChartPreview(document.querySelector('.history-chart-card.chart-fullscreen-fallback')); +}); + diff --git a/web/js/core.js b/web/js/core.js index 603a218..81591eb 100644 --- a/web/js/core.js +++ b/web/js/core.js @@ -34,7 +34,7 @@ const app = { customChartSeries: [], savedCharts: [], chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, controlPlan: null, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null, simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(), - flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false, + flowDraft: null, flowSelectedNodeId: null, flowSelectedNodeIds: [], flowConnectFrom: null, flowDirty: false, flowZoom: 1, }; try { app.savedCharts = JSON.parse(localStorage.getItem('gree_controller_saved_charts') || '[]'); } catch (_) { app.savedCharts = []; } diff --git a/web/js/entities.js b/web/js/entities.js index 7f6c7ee..8ad6c82 100644 --- a/web/js/entities.js +++ b/web/js/entities.js @@ -51,7 +51,7 @@ function deviceCard(device, detailed = false) { ${device.supports_turbo === false ? '' : ``}
${detailed ? deviceFeaturePanel(device) : ''} - ${detailed ? `` : ''} + ${detailed ? `` : ''} `; } @@ -307,7 +307,7 @@ function zoneCard(zone, detailed = true) {
${esc(sensorDetails)}
${compressorQueuePanel(zone)} ${manualTakeover} - + `; } @@ -389,7 +389,7 @@ function groupCard(group, detailed = false) {
${esc(tr('groups.mode'))}
${['house', 'heat', 'cool'].map(mode => ``).join('')}
${esc(tr('groups.profile'))}
${['auto', 'comfort', 'sleep', 'away'].map(preset => ``).join('')}
${customDraftPending ? `${esc(tr('groups.customDraftPending'))}` : ''}
- ${detailed ? `` : ''} + ${detailed ? `` : ''} `; } @@ -453,9 +453,9 @@ function renderSchedules() { $('#scheduleList').innerHTML = app.schedules.length ? app.schedules.map(item => { const zone = app.zones.find(z => z.id === item.zone_id); const days = item.weekdays.map(day => dayNames[day - 1]).join(', '); - return `

${esc(item.name)}

${esc(zone?.name || tr('common.noZone'))} · ${esc(days)}

${esc(item.enabled ? tr('common.enabled') : tr('common.disabled'))}
+ return `

${esc(item.name)}

${esc(zone?.name || tr('common.noZone'))} · ${esc(days)}

${esc(item.enabled ? tr('schedules.enabledState') : tr('schedules.disabledState'))}
${esc(tr('common.from'))}${esc(item.start_time)}
${esc(tr('common.to'))}${esc(item.end_time)}
${esc(tr('schedules.profile'))}${esc(item.preset === 'custom' ? fmtTemp(item.setpoint) : zonePresetLabel(item.preset))}
-
`; +
`; }).join('') : `
${esc(tr('schedules.emptyTitle'))}${esc(tr('schedules.emptyText'))}
`; } @@ -467,9 +467,9 @@ function renderAutomations() { const targetName = actionZone?.name || actionGroup?.name || actionDevice?.name || tr('common.noDevice'); const mode = item.action.mode === 'auto' && (actionGroup || actionZone) ? tr('groups.followHouse') : (item.action.mode ? modeLabel(item.action.mode) : '—'); const preset = item.action_zone_preset ? zonePresetLabel(item.action_zone_preset) : (item.action_preset ? zonePresetLabel(item.action_preset) : '—'); - return `

${esc(item.name)}

${esc(tr('automations.triggerSummary', { trigger: automationTriggerLabel(item), device: targetName }))}

${esc(item.enabled ? tr('common.active') : tr('common.disabled'))}
+ return `

${esc(item.name)}

${esc(tr('automations.triggerSummary', { trigger: automationTriggerLabel(item), device: targetName }))}

${esc(item.enabled ? tr('common.active') : tr('common.disabled'))}
${esc(tr('common.power'))}${item.action.power == null ? '—' : item.action.power ? tr('common.on') : tr('common.off')}
${esc(tr('common.mode'))}${esc(mode)}
${esc(tr('groups.profile'))}${esc(preset)}
${esc(tr('automations.last'))}${item.last_fired_at ? new Date(item.last_fired_at).toLocaleTimeString(locale(), { hour: '2-digit', minute: '2-digit' }) : '—'}
-
`; +
`; }).join('') : `
${esc(tr('automations.emptyTitle'))}${esc(tr('automations.emptyText'))}
`; } diff --git a/web/js/events.js b/web/js/events.js index 736f1de..a853582 100644 --- a/web/js/events.js +++ b/web/js/events.js @@ -49,6 +49,10 @@ document.addEventListener('click', async event => { } return; } + if (button.dataset.chartFullscreen) { + await toggleChartFullscreen(button.dataset.chartFullscreen); + return; + } if (button.dataset.chartZoom) { const id = button.dataset.chartId; const current = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM); diff --git a/web/js/flows.js b/web/js/flows.js index 01d92ff..8bd2fcf 100644 --- a/web/js/flows.js +++ b/web/js/flows.js @@ -97,10 +97,10 @@ function flowDefaultConfig(kind) { function renderFlows() { const host = $('#flowList'); if (!host) return; const count = $('#flowListCount'); if (count) count.textContent = tr('flow.listCount', { count: app.flows.length }); - host.innerHTML = app.flows.length ? app.flows.map(flow => `
-

${esc(flow.name)}${flow.draft ? ` ${esc(tr('flow.draft'))}` : ''}

${esc(flow.description || tr('flow.defaultDescription'))}

${flow.draft ? `` : ``}
+ host.innerHTML = app.flows.length ? app.flows.map(flow => `
+

${esc(flow.name)}${flow.draft ? ` ${esc(tr('flow.draft'))}` : ''}

${esc(flow.description || tr('flow.defaultDescription'))}

${flow.draft ? `` : ``}
${esc(tr('flow.blocks'))}${flow.nodes?.length || 0}
${esc(tr('nav.schedules'))}${flow.compiled_schedule_ids?.length || 0}
${esc(tr('nav.automations'))}${flow.compiled_automation_ids?.length || 0}
- +
`).join('') : `
${esc(tr('flow.emptyTitle'))}${esc(tr('flow.emptyText'))}
`; } @@ -150,13 +150,16 @@ function openFlowEditor(id = '', { push = true } = {}) { const flow = id ? app.flows.find(item => item.id === id) : null; if (id && !flow) return toast(tr('flow.notFound'), true); app.flowDraft = flowDraftFrom(flow); - app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false; + app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false; app.flowZoom = 1; app.flowNameEditing = !flow; renderFlowNameMode(); $('#flowEnabled').checked = app.flowDraft.enabled !== false; $('#flowEditor').hidden = false; document.body.classList.add('flow-editor-open'); + $('#flowNaturalPreview')?.classList.remove('is-expanded'); + $('#flowNaturalPreview .flow-preview-toggle')?.setAttribute('aria-expanded', 'false'); renderFlowEditor(); + requestAnimationFrame(() => { if (window.matchMedia('(max-width: 760px)').matches && app.flowDraft?.nodes?.length) fitFlowToView({ maxZoom: .9 }); }); startFlowSharedInputValueRefresh(); if (push) updateBrowserUrl(`/flows/${flow?.id || 'new'}`); } @@ -164,6 +167,8 @@ function openFlowEditor(id = '', { push = true } = {}) { function closeFlowEditor({ push = true, force = false } = {}) { if (!force && app.flowDirty && !confirm(tr('confirm.discardChanges'))) return false; stopFlowSharedInputValueRefresh(); + const mobileActions = $('#flowEditorActionsDialog'); if (mobileActions?.open) mobileActions.close(); + const blockDialog = $('#flowBlockDialog'); if (blockDialog?.open) blockDialog.close(); $('#flowEditor').hidden = true; document.body.classList.remove('flow-editor-open'); app.flowDraft = null; app.flowSelectedNodeId = null; app.flowSelectedNodeIds = []; app.flowConnectFrom = null; app.flowDirty = false; app.flowNameEditing = false; if (push) updateBrowserUrl('/flows'); @@ -346,6 +351,65 @@ function flowOperatorLabel(op) { return ({ lt: '<', lte: '≤', gt: '>', gte: ' function flowDurationLabel(seconds) { const value = Number(seconds || 0); return value >= 60 && value % 60 === 0 ? `${value / 60} min` : `${value} s`; } function flowNodeById(id) { return app.flowDraft?.nodes?.find(node => node.id === id); } +function renderFlowEnabledLabel() { + const input = $('#flowEnabled'), label = $('#flowEnabledLabel'); + if (input && label) label.textContent = tr(input.checked ? 'flow.enabledState' : 'flow.disabledState'); +} + +function renderFlowSaveStatus() { + const status = $('#flowSaveStatus'); + if (!status || !app.flowDraft) return; + const isNew = !app.flowDraft.id; + status.textContent = app.flowDirty ? tr('flow.unsavedChanges') : isNew ? tr('flow.notSavedYet') : tr('flow.savedState'); + status.classList.toggle('is-dirty', app.flowDirty || isNew); +} + +function setFlowZoom(value, { render = true } = {}) { + const next = clamp(Number(value) || 1, .45, 1.35); + app.flowZoom = Math.round(next * 20) / 20; + const canvas = $('#flowCanvas'); + if (canvas) canvas.style.zoom = String(app.flowZoom); + const label = $('#flowZoomLabel'); + if (label) label.textContent = `${Math.round(app.flowZoom * 100)}%`; + if (render) requestAnimationFrame(renderFlowEdges); +} + +function fitFlowToView({ maxZoom = 1 } = {}) { + const workspace = $('#flowWorkspace'); + const nodes = app.flowDraft?.nodes || []; + if (!workspace || !nodes.length) { setFlowZoom(1); if (workspace) workspace.scrollTo({ left:0, top:0, behavior:'smooth' }); return; } + const width = 170, height = 96, pad = 56; + const minX = Math.max(0, Math.min(...nodes.map(node => Number(node.x || 0))) - pad); + const minY = Math.max(0, Math.min(...nodes.map(node => Number(node.y || 0))) - pad); + const maxX = Math.max(...nodes.map(node => Number(node.x || 0) + width)) + pad; + const maxY = Math.max(...nodes.map(node => Number(node.y || 0) + height)) + pad; + const contentW = Math.max(1, maxX - minX), contentH = Math.max(1, maxY - minY); + const availableW = Math.max(220, workspace.clientWidth - 24), availableH = Math.max(180, workspace.clientHeight - 24); + const zoom = clamp(Math.min(availableW / contentW, availableH / contentH, maxZoom), .45, 1.35); + setFlowZoom(zoom); + requestAnimationFrame(() => workspace.scrollTo({ left:Math.max(0, minX * app.flowZoom - 12), top:Math.max(0, minY * app.flowZoom - 12), behavior:'smooth' })); +} + +function renderFlowBlockLibrary(filter = '') { + const host = $('#flowBlockLibrary'); if (!host) return; + const query = String(filter || '').trim().toLocaleLowerCase(locale()); + const groups = $$('.flow-palette .flow-palette-group'); + host.innerHTML = groups.map(group => { + const title = group.querySelector(':scope > span')?.textContent?.trim() || ''; + const buttons = $$('[data-flow-add]', group).filter(button => !query || `${title} ${button.textContent}`.toLocaleLowerCase(locale()).includes(query)); + if (!buttons.length) return ''; + const categoryClass = [...group.classList].find(name => name.startsWith('flow-palette-') && name !== 'flow-palette-group') || ''; + return `

${esc(title)}

${buttons.map(button => ``).join('')}
`; + }).join('') || `
${esc(tr('flow.noBlocksFound'))}${esc(tr('flow.noBlocksFoundHint'))}
`; +} + +function openFlowBlockLibrary() { + const search = $('#flowBlockSearch'); if (search) search.value = ''; + renderFlowBlockLibrary(''); + $('#flowBlockDialog')?.showModal(); + requestAnimationFrame(() => search?.focus()); +} + function renderFlowEditor() { const draft = app.flowDraft; if (!draft) return; const nodesHost = $('#flowNodes'); @@ -360,7 +424,7 @@ function renderFlowEditor() { }).join(''); $('#flowEmptyHint').hidden = draft.nodes.length > 0; const selectionCount = $('#flowSelectionCount'); if (selectionCount) selectionCount.textContent = (app.flowSelectedNodeIds || []).length ? tr('flow.selectedCount', { count:(app.flowSelectedNodeIds || []).length }) : ''; - renderFlowNameMode(); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); refreshFlowSharedInputCurrentValues(); + renderFlowNameMode(); renderFlowEnabledLabel(); setFlowZoom(app.flowZoom || 1, { render:false }); renderFlowEdges(); renderFlowInspector(); renderFlowInterpretation(); renderFlowRuntimeInfo(); renderFlowSaveStatus(); refreshFlowSharedInputCurrentValues(); const status = $('#flowCompileStatus'); status.textContent = draft.draft ? tr('flow.draftStatus') : tr('flow.compileCount', { schedules: draft.compiled_schedule_ids?.length || 0, automations: draft.compiled_automation_ids?.length || 0 }); status.classList.toggle('flow-draft-badge', draft.draft === true); @@ -368,13 +432,13 @@ function renderFlowEditor() { function renderFlowEdges() { const svg = $('#flowEdges'), workspace = $('#flowWorkspace'); if (!svg || !workspace || !app.flowDraft) return; - const rect = workspace.getBoundingClientRect(); - svg.setAttribute('viewBox', `0 0 ${Math.max(workspace.clientWidth, workspace.scrollWidth)} ${Math.max(workspace.clientHeight, workspace.scrollHeight)}`); + const rect = workspace.getBoundingClientRect(), zoom = app.flowZoom || 1; + svg.setAttribute('viewBox', '0 0 2400 1500'); svg.innerHTML = app.flowDraft.edges.map(edge => { const from = $(`[data-flow-node="${CSS.escape(edge.from)}"]`), to = $(`[data-flow-node="${CSS.escape(edge.to)}"]`); if (!from || !to) return ''; const a = from.getBoundingClientRect(), b = to.getBoundingClientRect(); - const x1 = a.right - rect.left + workspace.scrollLeft - 2, y1 = a.top - rect.top + workspace.scrollTop + a.height / 2; - const x2 = b.left - rect.left + workspace.scrollLeft + 2, y2 = b.top - rect.top + workspace.scrollTop + b.height / 2; + const x1 = (a.right - rect.left + workspace.scrollLeft) / zoom - 2, y1 = (a.top - rect.top + workspace.scrollTop + a.height / 2) / zoom; + const x2 = (b.left - rect.left + workspace.scrollLeft) / zoom + 2, y2 = (b.top - rect.top + workspace.scrollTop + b.height / 2) / zoom; const bend = Math.max(55, Math.abs(x2 - x1) * .45); return ``; }).join(''); @@ -401,7 +465,8 @@ function sharedFlowReferenceComparisonFields(item, config) { function renderFlowInspector() { const host = $('#flowInspector'), node = flowNodeById(app.flowSelectedNodeId); if (!host) return; - if (!node) { host.innerHTML = `
${esc(tr('flow.selectBlock'))}${esc(tr('flow.selectBlockHint'))}
`; return; } + host.classList.toggle('has-selection', Boolean(node)); + if (!node) { host.classList.remove('is-expanded'); host.innerHTML = `
${esc(tr('flow.selectBlock'))}${esc(tr('flow.selectBlockHint'))}
`; return; } const c = node.config || {}, meta = FLOW_NODE_META[node.kind] || { title: node.kind }; let fields = ''; if (node.kind === 'weekday') fields = `
${[1,2,3,4,5,6,7].map(day => ``).join('')}
`; @@ -440,8 +505,8 @@ function renderFlowInspector() { else if (node.kind === 'zone_thermostat') fields = `
`; else if (node.kind === 'device_action') fields = `${flowActionFields(c, false)}`; else if (node.kind === 'group_action') fields = `${flowActionFields(c, true)}`; - else if (node.kind === 'ha_service_action') fields = `
`; - host.innerHTML = `
${esc(tr('flow.blockSettings'))}

${esc(flowNodeTitle(meta))}

${fields}
ID${esc(node.id)}
`; + else if (node.kind === 'ha_service_action') fields = `

${esc(tr('flow.serviceExample'))}

`; + host.innerHTML = `
${esc(tr('flow.blockSettings'))}

${esc(flowNodeTitle(meta))}

${fields}
ID${esc(node.id)}
`; } function flowTextComparisonFields(c, numeric = false) { const options = numeric ? flowOperatorOptions(c.operator || 'eq') : [['eq','='],['neq','≠']].map(([v,l]) => ``).join(''); return `
`; } @@ -540,8 +605,12 @@ function flowUpstreamNodes(id) { function addFlowNode(kind) { if (!app.flowDraft || !FLOW_NODE_META[kind]) return; const count = app.flowDraft.nodes.length; - const node = { id: newFlowId('node'), kind, x: 80 + (count % 5) * 190, y: 70 + Math.floor(count / 5) * 130, config: flowDefaultConfig(kind) }; + const workspace = $('#flowWorkspace'); + const viewportX = workspace ? (workspace.scrollLeft / (app.flowZoom || 1)) + 48 : 80; + const viewportY = workspace ? (workspace.scrollTop / (app.flowZoom || 1)) + 54 : 70; + const node = { id: newFlowId('node'), kind, x: Math.max(36, viewportX + (count % 3) * 24), y: Math.max(36, viewportY + (count % 3) * 24), config: flowDefaultConfig(kind) }; app.flowDraft.nodes.push(node); app.flowSelectedNodeId = node.id; app.flowSelectedNodeIds = [node.id]; app.flowDirty = true; renderFlowEditor(); + requestAnimationFrame(() => $(`[data-flow-node="${CSS.escape(node.id)}"]`)?.scrollIntoView({ block:'center', inline:'center', behavior:'smooth' })); } function removeFlowNode(id) { if (!app.flowDraft) return; @@ -1071,7 +1140,7 @@ document.addEventListener('pointerdown', event => { }); document.addEventListener('pointermove', event => { if (!flowDrag || !app.flowDraft) return; - let dx = event.clientX - flowDrag.x, dy = event.clientY - flowDrag.y; + let dx = (event.clientX - flowDrag.x) / (app.flowZoom || 1), dy = (event.clientY - flowDrag.y) / (app.flowZoom || 1); const minLeft = Math.min(...flowDrag.starts.map(item => item.left)); const minTop = Math.min(...flowDrag.starts.map(item => item.top)); dx = Math.max(dx, 12 - minLeft); dy = Math.max(dy, 12 - minTop); @@ -1080,11 +1149,13 @@ document.addEventListener('pointermove', event => { node.x = start.left + dx; node.y = start.top + dy; const el = $(`[data-flow-node="${CSS.escape(node.id)}"]`); if (el) { el.style.left = `${node.x}px`; el.style.top = `${node.y}px`; } }); - app.flowDirty = true; renderFlowEdges(); + app.flowDirty = true; renderFlowSaveStatus(); renderFlowEdges(); }); document.addEventListener('pointerup', () => { flowDrag = null; }); document.addEventListener('keydown', event => { + const flowCard = event.target.closest?.('[data-flow-card-id]'); + if (flowCard && event.target === flowCard && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); openFlowEditor(flowCard.dataset.flowCardId); return; } if ($('#flowEditor')?.hidden || !app.flowDraft) return; if (event.target.closest?.('input,select,textarea,[contenteditable="true"]')) return; if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') { event.preventDefault(); selectAllFlowNodes(); return; } @@ -1092,8 +1163,10 @@ document.addEventListener('keydown', event => { }); document.addEventListener('click', event => { + const flowCard = event.target.closest?.('[data-flow-card-id]'); + if (flowCard && !event.target.closest?.('button,summary,details,input,select,textarea,a,label')) { openFlowEditor(flowCard.dataset.flowCardId); return; } const edge = event.target.closest?.('[data-flow-edge]'); if (edge) { removeFlowEdge(edge.dataset.flowEdge); return; } - const add = event.target.closest?.('[data-flow-add]'); if (add) { addFlowNode(add.dataset.flowAdd); return; } + const add = event.target.closest?.('[data-flow-add]'); if (add) { addFlowNode(add.dataset.flowAdd); const dialog = add.closest?.('#flowBlockDialog'); if (dialog?.open) dialog.close(); return; } const templateCategory = event.target.closest?.('[data-flow-template-category]'); if (templateCategory) { renderFlowPresetBrowser(templateCategory.dataset.flowTemplateCategory); return; } const templateFavorite = event.target.closest?.('[data-flow-template-favorite]'); if (templateFavorite) { toggleFlowPresetFavorite(templateFavorite.dataset.flowTemplateFavorite); return; } const templatePreview = event.target.closest?.('[data-flow-template-preview]'); if (templatePreview) { previewFlowTemplate(templatePreview.dataset.flowTemplatePreview); return; } @@ -1103,11 +1176,24 @@ document.addEventListener('click', event => { const input = event.target.closest?.('[data-flow-input]'); if (input) { if (app.flowConnectFrom) connectFlowNodes(app.flowConnectFrom, input.dataset.flowInput); return; } const node = event.target.closest?.('[data-flow-node]'); if (node) { if (!(app.flowSelectedNodeIds || []).includes(node.dataset.flowNode)) app.flowSelectedNodeIds = [node.dataset.flowNode]; app.flowSelectedNodeId = node.dataset.flowNode; renderFlowEditor(); return; } const actionButton = event.target.closest?.('[data-action]'); const action = actionButton?.dataset.action; + const mobileActionsDialog = actionButton?.closest?.('#flowEditorActionsDialog'); + if (mobileActionsDialog?.open && action !== 'flow-mobile-actions') mobileActionsDialog.close(); if (action === 'new-flow') openFlowEditor(); else if (action === 'edit-flow') openFlowEditor(actionButton.dataset.id); else if (action === 'delete-flow') deleteFlow(actionButton.dataset.id); else if (action === 'toggle-flow-enabled') toggleFlowEnabled(actionButton.dataset.id, actionButton.dataset.value === 'true'); else if (action === 'save-flow') saveFlow(); + else if (action === 'flow-mobile-actions') $('#flowEditorActionsDialog')?.showModal(); + else if (action === 'flow-add-block') openFlowBlockLibrary(); + else if (action === 'flow-zoom-out') setFlowZoom((app.flowZoom || 1) - .1); + else if (action === 'flow-zoom-in') setFlowZoom((app.flowZoom || 1) + .1); + else if (action === 'flow-fit') fitFlowToView(); + else if (action === 'flow-preview-toggle') { + const preview = $('#flowNaturalPreview'); + const expanded = preview?.classList.toggle('is-expanded') || false; + actionButton.setAttribute('aria-expanded', String(expanded)); + } + else if (action === 'flow-toggle-inspector') $('#flowInspector')?.classList.toggle('is-expanded'); else if (action === 'import-flow') $('#flowImportFile')?.click(); else if (action === 'export-flow') exportFlow(); else if (action === 'flow-templates') openFlowTemplates(); @@ -1134,7 +1220,8 @@ document.addEventListener('change', event => { }); $('#flowImportFile')?.addEventListener('change', event => importFlowFile(event.target.files?.[0])); $('#flowTemplateSearch')?.addEventListener('input', event => { flowPresetSearch = event.target.value || ''; renderFlowPresetBrowser(flowPresetActiveCategory); }); -$('#flowName')?.addEventListener('input', () => { if (app.flowDraft) app.flowDirty = true; }); +$('#flowBlockSearch')?.addEventListener('input', event => renderFlowBlockLibrary(event.target.value)); +$('#flowName')?.addEventListener('input', () => { if (app.flowDraft) { app.flowDirty = true; renderFlowSaveStatus(); } }); $('#flowName')?.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); commitFlowName(); } else if (event.key === 'Escape') { event.preventDefault(); $('#flowName').value = app.flowDraft?.name || ''; app.flowNameEditing = false; renderFlowNameMode(); } @@ -1143,5 +1230,5 @@ $('#flowName')?.addEventListener('blur', () => { if (!app.flowDraft || !app.flowNameEditing) return; if ($('#flowName').value.trim()) commitFlowName(); }); -$('#flowEnabled')?.addEventListener('change', () => { if (app.flowDraft) app.flowDirty = true; }); +$('#flowEnabled')?.addEventListener('change', () => { if (app.flowDraft) { app.flowDirty = true; renderFlowEnabledLabel(); renderFlowSaveStatus(); } }); window.addEventListener('resize', () => { if (!$('#flowEditor')?.hidden) renderFlowEdges(); }); diff --git a/web/js/navigation.js b/web/js/navigation.js index 540adc5..fcde5f0 100644 --- a/web/js/navigation.js +++ b/web/js/navigation.js @@ -19,7 +19,7 @@ function showView(name, { push = true, scroll = true } = {}) { if (name !== 'flows' && !$('#flowEditor')?.hidden) closeFlowEditor({ push: false, force: true }); app.currentView = name; $$('.view').forEach(view => view.classList.toggle('active', view.dataset.view === name)); - $$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['groups', 'flows', 'schedules', 'automations', 'simulation', 'night', 'homeassistant', 'settings', 'logs'].includes(name)))); + $$('.bottom-nav button').forEach(button => button.classList.toggle('active', button.dataset.nav === name || (button.dataset.nav === 'more' && ['devices', 'groups', 'schedules', 'automations', 'simulation', 'night', 'homeassistant', 'settings', 'logs'].includes(name)))); if (push) updateBrowserUrl(pathForView(name)); if (scroll) window.scrollTo({ top: 0, behavior: 'smooth' }); if (name === 'dashboard') setDashboardTab(app.dashboardTab, { scroll: false }); diff --git a/web/js/settings-ui.js b/web/js/settings-ui.js index 3f3a758..31cf736 100644 --- a/web/js/settings-ui.js +++ b/web/js/settings-ui.js @@ -8,6 +8,18 @@ function renderAccessTokens() { `).join('') : `
${esc(tr('settings.noTokens'))}${esc(tr('settings.noTokensHint'))}
`; } +function metricHaEntities() { + return new Set([ + app.settings?.home_assistant?.outdoor_entity_id, + ...app.zones.filter(zone => ['home_assistant', 'combined'].includes(zone.sensor_source)).map(zone => zone.ha_entity_id), + ...app.historyData.sensors.map(row => row.entity_id), + ].filter(Boolean)); +} + +function flowHaEntities() { + return new Set((app.flowSharedInputs || []).map(item => item?.config?.entity_id).filter(Boolean)); +} + function knownHaEntities() { return [...new Set([ ...Object.keys(app.sensorAliases || {}), @@ -22,7 +34,11 @@ function knownHaEntities() { function renderSensorAliases() { const host = $('#sensorAliasList'); if (!host) return; const entities = knownHaEntities(); - host.innerHTML = entities.length ? entities.map(entity => `
${esc(entity)}
`).join('') : `
${esc(tr('settings.noSensorAliases'))}
`; + const metricEntities = metricHaEntities(), flowEntities = flowHaEntities(); + host.innerHTML = entities.length ? entities.map(entity => { + const badges = `${metricEntities.has(entity) ? `${esc(tr('settings.sensorMetricBadge'))}` : ''}${flowEntities.has(entity) ? `${esc(tr('settings.sensorFlowBadge'))}` : ''}`; + return `
${esc(entity)}${badges ? `${badges}` : ''}
`; + }).join('') : `
${esc(tr('settings.noSensorAliases'))}
`; } function flowSharedInputKinds() { diff --git a/web/styles.css b/web/styles.css index 440815e..f4d6504 100644 --- a/web/styles.css +++ b/web/styles.css @@ -35,7 +35,7 @@ --grid: rgba(255, 255, 255, .07); --scroll-thumb: #444444; --scroll-thumb-hover: #575757; - --radius: 22px; + --radius: 16px; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } @@ -6180,7 +6180,7 @@ body.flow-editor-open { overflow:hidden; } .flow-editor-bar { min-height:76px; display:flex; align-items:center; justify-content:space-between; gap:20px; padding:10px 18px; border-bottom:1px solid var(--line); background:var(--surface); } .flow-editor-title { display:flex; align-items:center; gap:12px; min-width:0; } .flow-editor-title > div { display:grid; gap:2px; min-width:0; } -.flow-name-wrap { min-width:280px; min-height:36px; display:flex; align-items:center; } +.flow-name-wrap { min-width:280px; min-height:36px; display:flex; align-items:center; gap:8px; } .flow-name-view { display:flex; align-items:center; gap:6px; min-width:0; } .flow-name-view[hidden] { display:none !important; } .flow-name-text { appearance:none; border:0; background:transparent; color:var(--text); font:inherit; font-weight:750; font-size:1.12rem; line-height:1.25; padding:4px 2px; min-width:0; max-width:min(52vw,620px); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:text; text-align:left; } @@ -6426,3 +6426,338 @@ body.flow-editor-open { overflow:hidden; } .flow-template-preview-node-trigger { border-top:3px solid var(--warning); } .flow-template-preview-node-time, .flow-template-preview-node-timeop { border-top:3px solid var(--blue); } .flow-template-preview-node-haaction { border-top:3px solid var(--accent); } + + +/* Compact Flow state control and mobile action entry point. */ +.flow-enabled { white-space:nowrap; } +.flow-enabled input { width:16px; min-height:16px; accent-color:var(--accent); } +.flow-mobile-actions-button { display:none; } +.flow-inspector-head-actions { display:flex; align-items:center; gap:7px; } +.flow-inspector-close { display:none; width:34px; height:34px; padding:0; font-size:1.1rem; } + +/* Clarify which HA aliases are metric sources and which are only Flow references. */ +.sensor-alias-entity { min-width:0; display:flex; align-items:center; gap:7px; } +.sensor-alias-entity > .mono { min-width:0; overflow:hidden; color:var(--muted); font-size:11px; text-overflow:ellipsis; white-space:nowrap; } +.sensor-source-badges { display:flex; flex:0 0 auto; gap:4px; } +.sensor-source-badge { display:inline-flex; align-items:center; min-height:20px; padding:2px 6px; border:1px solid var(--line); border-radius:999px; font-size:.64rem; font-weight:750; line-height:1; white-space:nowrap; } +.sensor-source-badge.metrics { border-color:var(--accent-border); background:var(--accent-soft); color:var(--accent); } +.sensor-source-badge.flow { background:var(--surface); color:var(--muted); } + +/* UX refresh: clearer hierarchy, safer destructive actions and touch-first layouts. */ +:root { --radius-small:10px; --radius-medium:14px; } + +.device-card,.panel,.list-card { border-radius:var(--radius); } +button { border-radius:11px; } +.metric { border-radius:14px; } +.lead { max-width:780px; line-height:1.55; } +.section-heading h1 { margin-bottom:0; } + +.automation-tabs { + display:flex; align-items:center; gap:5px; width:max-content; max-width:100%; + margin:-2px 0 14px; padding:5px; overflow-x:auto; border:1px solid var(--line); + border-radius:14px; background:var(--surface); scrollbar-width:none; +} +.automation-tabs::-webkit-scrollbar { display:none; } +.automation-tabs button { flex:0 0 auto; min-height:38px; padding:8px 12px; color:var(--muted); background:transparent; font-size:12px; font-weight:750; } +.automation-tabs button.active { color:var(--accent); background:var(--accent-soft); box-shadow:inset 0 0 0 1px var(--accent-border); } + +.mobile-nav-only { display:block; } +.desktop-nav-only { display:none; } +.nav-section-label,.menu-section-label { color:var(--muted); font-size:9px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; } +.menu-section-label { display:block; padding:14px 9px 5px; } +.more-menu-list .menu-section-label:first-child { padding-top:5px; } + +.card-menu { align-items:center; } +.card-primary-action { font-weight:700; } +.card-overflow { position:relative; flex:0 0 auto; } +.card-overflow summary { + display:grid; place-items:center; width:38px; height:38px; border:1px solid var(--line); + border-radius:10px; color:var(--muted); background:var(--surface-muted); cursor:pointer; + list-style:none; font-size:16px; font-weight:800; letter-spacing:.08em; +} +.card-overflow summary::-webkit-details-marker { display:none; } +.card-overflow[open] summary { color:var(--text); border-color:var(--line-strong); background:var(--surface-2); } +.card-overflow-menu { + position:absolute; z-index:25; right:0; bottom:calc(100% + 7px); min-width:150px; padding:6px; + border:1px solid var(--line); border-radius:12px; background:var(--surface); box-shadow:0 14px 34px rgba(0,0,0,.28); +} +.card-overflow-menu button { width:100%; text-align:left; } +.card-overflow-menu .danger { background:var(--danger-soft); } + +.flow-save-status { display:inline-flex; align-items:center; min-height:24px; color:var(--muted); font-size:.72rem; font-weight:750; white-space:nowrap; } +.flow-save-status::before { content:""; width:6px; height:6px; margin-right:6px; border-radius:50%; background:var(--accent); } +.flow-save-status.is-dirty { color:var(--warning); } +.flow-save-status.is-dirty::before { background:var(--warning); } +.flow-canvas { position:relative; width:2400px; height:1500px; transform-origin:0 0; } +.flow-canvas .flow-nodes,.flow-canvas .flow-edges { width:2400px; height:1500px; } +.flow-workspace-toolbar { justify-content:space-between; } +.flow-workspace-primary-actions,.flow-zoom-controls { display:flex; align-items:center; gap:7px; min-width:0; } +.flow-add-block-button { display:none; align-items:center; justify-content:center; gap:5px; } +.flow-zoom-controls .icon-button { width:32px; height:32px; min-height:32px; padding:0; font-size:17px; } +.flow-zoom-controls > span { min-width:44px; color:var(--muted); font-size:.72rem; font-weight:800; text-align:center; font-variant-numeric:tabular-nums; } +.flow-fit-button { min-height:32px; padding:6px 9px; font-size:.75rem; } +.flow-inspector-expand { display:none; width:34px; height:34px; padding:0; } + +.flow-block-dialog { width:min(620px,calc(100% - 24px)); } +.flow-block-dialog .dialog-head { padding:16px 16px 8px; } +.flow-block-search { padding:4px 16px 10px; } +.flow-block-library { display:grid; gap:14px; padding:0 16px 18px; } +.flow-block-library-group { padding-left:10px; border-left:3px solid var(--line-strong); } +.flow-block-library-group.flow-palette-trigger { border-left-color:var(--warning); } +.flow-block-library-group.flow-palette-time { border-left-color:var(--blue); } +.flow-block-library-group.flow-palette-timeop { border-left-color:var(--info); } +.flow-block-library-group.flow-palette-sensor { border-left-color:var(--teal); } +.flow-block-library-group.flow-palette-logic { border-left-color:var(--purple); } +.flow-block-library-group.flow-palette-action { border-left-color:var(--orange); } +.flow-block-library-group.flow-palette-haaction { border-left-color:var(--accent); } +.flow-block-library-group h3 { margin:0 0 7px; color:var(--muted); font-size:.72rem; letter-spacing:.07em; text-transform:uppercase; } +.flow-block-library-group > div { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; } +.flow-block-library-group button { min-height:42px; text-align:left; } + +/* Port stays visually small but gets a comfortable touch target. */ +.flow-editor .flow-port { width:16px; height:16px; margin-top:-8px; } +.flow-editor .flow-port-in { left:-9px; } +.flow-editor .flow-port-out { right:-9px; } +.flow-editor .flow-port::after { content:""; position:absolute; inset:-11px; border-radius:50%; } + +.flow-preview-toggle { display:none; } +.flow-preview-details { display:flex; justify-content:space-between; gap:24px; align-items:center; width:100%; min-width:0; } + +@media (min-width:900px) { + .mobile-nav-only { display:none !important; } + .desktop-nav-only { display:flex; } + .bottom-nav { + top:calc(var(--topbar-height,72px) + 14px); bottom:14px; left:max(14px,calc((100vw - 1440px)/2)); + display:flex; flex-direction:column; gap:2px; width:190px; max-height:calc(100dvh - var(--topbar-height,72px) - 28px); + padding:10px; overflow-y:auto; border-radius:16px; scrollbar-width:thin; + } + .bottom-nav .nav-section-label { padding:10px 9px 4px; } + .bottom-nav .nav-section-label:first-child { padding-top:3px; } + .bottom-nav button { display:flex; align-items:center; justify-content:flex-start; gap:10px; min-height:39px; padding:8px 10px; border-radius:10px; text-align:left; } + .bottom-nav button span { display:grid; place-items:center; flex:0 0 24px; width:24px; font-size:17px; } + .bottom-nav button b { min-width:0; overflow:hidden; font-size:11px; text-overflow:ellipsis; white-space:nowrap; } + .bottom-nav button.active { background:var(--accent-soft); } + main { width:min(1440px,100%); padding-left:222px; padding-right:24px; } +} + +@media (max-width:760px) { + main { padding:18px 12px calc(94px + env(safe-area-inset-bottom)); } + .desktop-nav-only { display:none !important; } + .mobile-nav-only { display:grid; } + .bottom-nav { grid-template-columns:repeat(5,minmax(0,1fr)); } + .bottom-nav button { min-height:48px; } + .bottom-nav button span { font-size:19px; } + + .section-heading { gap:12px; margin:4px 0 14px; } + .section-heading h1 { font-size:26px; } + .section-heading > button,.section-heading > .section-actions,.section-heading > .section-heading-actions { width:100%; } + .section-heading > button { min-height:44px; } + .flow-page-actions,.section-heading-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; } + .flow-page-actions button,.section-heading-actions button { width:100%; min-height:44px; } + .automation-tabs { width:100%; margin-bottom:12px; scroll-snap-type:x proximity; } + .automation-tabs button { flex:1 0 auto; scroll-snap-align:start; } + + .device-card,.panel,.list-card { border-radius:14px; } + .device-card { padding:15px; } + .card-footer { gap:9px; } + .card-menu { width:100%; justify-content:flex-end; } + .card-menu > button { min-height:40px; } + .card-primary-action { flex:1 1 auto; } + .card-overflow summary { width:40px; height:40px; } + + dialog:not(.auth-dialog) { + inset:auto 0 0; width:100%; max-width:none; max-height:88dvh; margin:0; + border-right:0; border-bottom:0; border-left:0; border-radius:18px 18px 0 0; + overscroll-behavior:contain; + } + dialog:not(.auth-dialog) .dialog-head { position:sticky; top:0; z-index:5; padding-top:14px; background:var(--dialog-bg); } + dialog:not(.auth-dialog) .dialog-head::before { content:""; position:absolute; top:6px; left:50%; width:38px; height:4px; border-radius:999px; background:var(--line-strong); transform:translateX(-50%); } + .dialog-form { padding:14px 14px calc(14px + env(safe-area-inset-bottom)); } + .dialog-form .form-actions { position:sticky; bottom:0; z-index:4; margin:6px -14px -14px; padding:10px 14px calc(10px + env(safe-area-inset-bottom)); border-top:1px solid var(--line); background:color-mix(in srgb,var(--dialog-bg) 96%,transparent); backdrop-filter:blur(12px); } + .dialog-form .form-actions > button { flex:1 1 0; min-height:44px; } + + .history-tabs { margin-inline:-12px; padding-inline:12px; scroll-padding-inline:12px; scroll-snap-type:x proximity; } + .history-tabs button { min-height:40px; scroll-snap-align:start; } + .history-toolbar-panel { padding:12px; } + + .settings-mode-tabs { position:sticky; top:calc(var(--topbar-height,64px) + 6px); z-index:8; margin-inline:-4px; background:color-mix(in srgb,var(--bg) 94%,transparent); backdrop-filter:blur(12px); } + .settings-save-bar { bottom:calc(74px + env(safe-area-inset-bottom)); margin-inline:-4px; padding:10px; } + .settings-save-bar span { display:none; } + .settings-save-bar button { width:100%; min-height:44px; } + + /* Mobile Flow: compact header, canvas-first editor and bottom-sheet inspector. */ + .flow-editor-bar { + display:grid; grid-template-columns:minmax(0,1fr) auto; grid-template-rows:auto auto; + align-items:center; width:100%; gap:6px 8px; padding:7px 8px; overflow:hidden; + } + .flow-editor-title { grid-column:1; grid-row:1; width:100%; min-width:0; gap:8px; } + .flow-editor-title > div { width:100%; min-width:0; } + .flow-editor-title .eyebrow { display:none; } + .flow-name-wrap { display:flex; width:100%; min-width:0; gap:6px; } + .flow-name-view { min-width:0; flex:1 1 auto; } + .flow-name-text { min-width:0; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .flow-editor-title input { width:100%; min-width:0; flex:1 1 auto; font-size:1rem; } + .flow-editor-bar > .flow-save-button { + grid-column:2; grid-row:1; align-self:stretch; min-width:0; min-height:38px; + padding-inline:11px; white-space:nowrap; + } + .flow-editor-actions { + grid-column:1 / -1; grid-row:2; display:flex; align-items:center; justify-content:space-between; + width:100%; min-width:0; max-width:none; gap:8px; overflow:hidden; + } + .flow-editor-actions .flow-desktop-action,.flow-save-status,.flow-compile-status { display:none; } + .flow-editor-actions .flow-enabled { + display:flex; flex:0 1 auto; width:auto; min-width:0; min-height:34px; + justify-content:flex-start; gap:7px; padding:0 4px; font-size:.78rem; + } + .flow-editor-actions .flow-enabled span { display:inline; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .flow-editor-actions .flow-mobile-actions-button { + display:inline-flex; flex:0 0 auto; width:auto; min-width:96px; min-height:34px; + margin-left:auto; padding:6px 12px; justify-content:center; + } + + .flow-editor-body { position:relative; display:flex; flex:1 1 auto; flex-direction:column; height:0; min-width:0; min-height:0; overflow:hidden; } + .flow-workspace-wrap { + order:1; display:grid; grid-template-rows:auto minmax(0,1fr) auto; flex:1 1 auto; + width:100%; height:100%; min-width:0; min-height:0; overflow:hidden; + } + .flow-workspace-toolbar { display:flex; flex:0 0 auto; width:100%; min-width:0; min-height:48px; padding:6px 8px; gap:7px; justify-content:space-between; overflow:hidden; } + .flow-workspace-primary-actions { display:flex; flex:1 1 auto; min-width:0; overflow:hidden; } + .flow-add-block-button { display:inline-flex; min-width:0; max-width:128px; min-height:36px; padding:7px 10px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .flow-selection-action,#flowSelectionCount { display:none; } + .flow-zoom-controls { display:flex; flex:0 0 auto; margin-left:auto; gap:4px; } + .flow-zoom-controls .icon-button { width:34px; height:34px; min-height:34px; } + .flow-zoom-controls > span { display:none; } + .flow-fit-button { width:34px; min-width:34px; min-height:34px; padding:0; font-size:0; } + .flow-fit-button::before { content:"⊡"; font-size:17px; line-height:1; } + .flow-palette { display:none !important; } + .flow-workspace { height:100%; min-width:0; min-height:0 !important; max-height:none; overflow:auto; overscroll-behavior:contain; touch-action:pan-x pan-y; } + + .flow-natural-preview { display:block; flex:0 0 auto; min-height:0; max-height:none; padding:0; overflow:hidden; } + .flow-preview-toggle { + display:flex; align-items:center; justify-content:space-between; width:100%; min-height:38px; + padding:7px 10px; border:0; border-radius:0; background:var(--surface); color:var(--text); text-align:left; + } + .flow-preview-toggle strong { font-size:.78rem; } + .flow-preview-toggle-icon { color:var(--muted); font-size:1rem; transition:transform .15s ease; } + .flow-preview-details { display:none; flex-direction:column; align-items:stretch; gap:7px; max-height:32dvh; padding:8px 10px 10px; overflow:auto; } + .flow-natural-preview.is-expanded .flow-preview-details { display:flex; } + .flow-natural-preview.is-expanded .flow-preview-toggle { border-bottom:1px solid var(--line); } + .flow-natural-preview.is-expanded .flow-preview-toggle-icon { transform:rotate(180deg); } + .flow-preview-item { gap:1px; } + .flow-natural-preview strong { font-size:.78rem; } + .flow-natural-preview span { font-size:.76rem; } + + .flow-inspector { + display:none; position:fixed; inset:auto 0 0; width:100%; max-height:56dvh; + padding:22px 14px calc(14px + env(safe-area-inset-bottom)); overflow:auto; + border:0; border-top:1px solid var(--line-strong); border-radius:18px 18px 0 0; + z-index:95; box-shadow:0 -14px 34px rgba(0,0,0,.3); transition:max-height .18s ease; + } + .flow-inspector.has-selection { display:block; } + .flow-inspector::before { content:""; position:absolute; top:7px; left:50%; width:38px; height:4px; border-radius:999px; background:var(--line-strong); transform:translateX(-50%); } + .flow-inspector.is-expanded { max-height:90dvh; } + .flow-inspector-close,.flow-inspector-expand { display:inline-grid; place-items:center; } + .flow-inspector-head { position:sticky; top:-22px; z-index:4; margin:-8px -2px 14px; padding:10px 2px 8px; background:var(--surface); } + .flow-inspector-delete { padding-inline:10px; } + .flow-inspector .two { grid-template-columns:1fr; } + .flow-inspector label { font-size:.78rem; } + .flow-inspector input,.flow-inspector select,.flow-inspector textarea { min-height:44px; } + + .flow-block-dialog { max-height:82dvh !important; } + .flow-block-library-group > div { grid-template-columns:1fr; } + .flow-block-library-group button { min-height:46px; } + + .sensor-alias-row { grid-template-columns:minmax(0,1fr) 34px; } + .sensor-alias-entity { grid-column:1 / -1; } + .sensor-alias-row input { grid-column:1; } + .sensor-alias-clear { grid-column:2; } + + /* Do not let long settings/help copy dominate small screens. */ + .settings-block { gap:14px; } + .settings-block-head h3 { font-size:16px; } + .settings-block-head p,.field-note { line-height:1.45; } +} + +@media (max-width:420px) { + .section-heading h1 { font-size:24px; } + .flow-editor-actions .flow-mobile-actions-button { min-width:88px; padding-inline:10px; } +} + +/* Readability and form consistency fixes found during the wider UX pass. */ +textarea { font:inherit; } +textarea { + width:100%; min-height:96px; resize:vertical; border:1px solid var(--line); border-radius:12px; + outline:0; padding:10px 12px; color:var(--text); background:var(--input-bg); line-height:1.45; +} +textarea:focus { border-color:var(--accent); outline:2px solid color-mix(in srgb,var(--accent) 18%,transparent); outline-offset:1px; } +.flow-empty-hint button { pointer-events:auto; margin-top:7px; justify-self:center; } +#controlPlan .plan-card>p { font-size:11.5px; line-height:1.45; } +#controlPlan .plan-events li { font-size:10.5px; line-height:1.4; } +#controlPlan .plan-rule-summary strong { font-size:11px; } +#controlPlan .plan-rule-summary small { font-size:10px; } +#controlPlan .plan-card-head .eyebrow { font-size:9px; } + +@media (prefers-reduced-motion:reduce) { + *,*::before,*::after { scroll-behavior:auto !important; animation-duration:.01ms !important; animation-iteration-count:1 !important; transition-duration:.01ms !important; } +} +.flow-card[data-flow-card-id] { cursor:pointer; } +.flow-card[data-flow-card-id]:focus-visible { outline:2px solid color-mix(in srgb,var(--accent) 55%,transparent); outline-offset:2px; } +.flow-card[data-flow-card-id] button,.flow-card[data-flow-card-id] details { cursor:pointer; } +.flow-generated-card { border-left:3px solid var(--accent-border); } +.flow-generated-card .card-footer > small { color:var(--accent); } + + +/* 0.11.4 production layout guards and in-app chart preview. */ +html,body { max-width:100%; overflow-x:clip; } +main,.view { min-width:0; max-width:100%; } +.flow-info-panel,.flow-info-panel > div { min-width:0; } +.flow-info-panel > .badge { flex:0 1 auto; max-width:100%; white-space:normal; overflow-wrap:anywhere; text-align:center; } + +body.flow-editor-open { width:100%; height:100vh; height:100dvh; overflow:hidden; overscroll-behavior:none; } +.flow-editor { width:100vw; width:100dvw; height:100vh; height:100dvh; max-width:100%; overflow:hidden; } +.flow-editor-bar,.flow-editor-title,.flow-editor-actions,.flow-editor-body,.flow-workspace-wrap,.flow-workspace-toolbar { min-width:0; max-width:100%; } + +.chart-title-row > div:first-child { min-width:0; } +.chart-title-actions { display:flex; align-items:center; gap:7px; flex:0 0 auto; } +.chart-fullscreen-button { min-width:34px; height:32px; padding:0 9px; border:1px solid var(--line); background:var(--surface-muted); color:var(--text-soft); font-size:16px; line-height:1; } +.chart-fullscreen-button:hover { background:var(--surface-2); } +body.chart-fullscreen-open { overflow:hidden; } +body.chart-fullscreen-open::before { content:""; position:fixed; inset:0; z-index:139; background:rgba(0,0,0,.68); backdrop-filter:blur(3px); } +.history-chart-card.chart-fullscreen-fallback { + position:fixed; left:50%; top:50%; z-index:140; + width:min(1200px,calc(100vw - 28px)); height:min(820px,calc(100dvh - 28px)); max-width:none; + margin:0; padding:14px; transform:translate(-50%,-50%); + display:grid; grid-template-rows:auto minmax(0,1fr) auto; gap:8px; overflow:hidden; + border:1px solid var(--line-strong); border-radius:16px; background:var(--surface); + box-shadow:0 28px 90px rgba(0,0,0,.58); +} +.history-chart-card.chart-fullscreen-fallback .chart-title-row { margin:0; } +.history-chart-card.chart-fullscreen-fallback .chart-title-row p, +.history-chart-card.chart-fullscreen-fallback .chart-hover-hint { display:none; } +.history-chart-card.chart-fullscreen-fallback .chart-wrap { height:100%; min-height:0; overflow:auto; border:1px solid var(--line); border-radius:10px; background:var(--surface-muted); } +.history-chart-card.chart-fullscreen-fallback .legend { max-height:88px; margin:0; overflow:auto; } + +@media (max-width:760px) { + body:not(.flow-editor-open) .bottom-nav { position:fixed; right:0; bottom:0; left:0; z-index:60; max-width:100vw; overflow-x:hidden; } + .flow-info-panel { flex-direction:column; align-items:flex-start; gap:12px; padding:16px; overflow:hidden; } + .flow-info-panel > div { width:100%; } + .flow-info-panel > .badge { align-self:flex-start; text-align:left; } + .flow-shared-settings-link { width:100%; max-width:100%; white-space:normal; text-align:left; } + .automation-tabs { width:100%; min-width:0; } + + .chart-title-row { gap:8px; } + .chart-title-actions { width:100%; justify-content:flex-end; } + .chart-fullscreen-button { min-height:36px; height:36px; } + .history-chart-card.chart-fullscreen-fallback { width:calc(100vw - 12px); height:calc(100dvh - 18px); padding:9px; border-radius:13px; } + .history-chart-card.chart-fullscreen-fallback .chart-title-row { flex-direction:row; align-items:center; gap:8px; } + .history-chart-card.chart-fullscreen-fallback .chart-title-row > div:first-child { flex:1 1 auto; min-width:0; } + .history-chart-card.chart-fullscreen-fallback .chart-title-row h3 { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .history-chart-card.chart-fullscreen-fallback .chart-title-actions { width:auto; flex:0 0 auto; } + .history-chart-card.chart-fullscreen-fallback .legend { max-height:70px; } +} + +@media (max-width:560px) { + .chart-title-actions { gap:5px; } + .chart-zoom-controls button { height:36px; } +}