Tion 4S
+ Загрузка + — +Скорость вентиляции
Текущая ступень вентилятора
Цель (температура)
Желаемая температура воздуха
Питание
Устройство выключено
Обогрев
Поддержание температуры
Показатели воздуха
Ожидаем показания датчика
+в помещении
diff --git a/web/assets/icons.svg b/web/assets/icons.svg
new file mode 100644
index 0000000..926d1e0
--- /dev/null
+++ b/web/assets/icons.svg
@@ -0,0 +1,47 @@
+
diff --git a/web/assets/mountain-lake-dark.jpg b/web/assets/mountain-lake-dark.jpg
new file mode 100644
index 0000000..5200d87
Binary files /dev/null and b/web/assets/mountain-lake-dark.jpg differ
diff --git a/web/assets/mountain-lake-light.jpg b/web/assets/mountain-lake-light.jpg
new file mode 100644
index 0000000..a44c5f0
Binary files /dev/null and b/web/assets/mountain-lake-light.jpg differ
diff --git a/web/css/base.css b/web/css/base.css
new file mode 100644
index 0000000..d7da39c
--- /dev/null
+++ b/web/css/base.css
@@ -0,0 +1,293 @@
+:root {
+ color-scheme: light;
+ --page: #dcecf2;
+ --page-deep: #b7d4df;
+ --glass: rgba(244, 251, 253, 0.58);
+ --glass-strong: rgba(248, 253, 255, 0.76);
+ --glass-soft: rgba(255, 255, 255, 0.34);
+ --line: rgba(255, 255, 255, 0.72);
+ --line-soft: rgba(63, 104, 121, 0.14);
+ --text: #17313e;
+ --muted: #617b87;
+ --faint: #8ca0a9;
+ --blue: #198ed1;
+ --blue-deep: #096ca7;
+ --blue-soft: rgba(25, 142, 209, 0.14);
+ --green: #20a66a;
+ --green-soft: rgba(32, 166, 106, 0.14);
+ --orange: #e98a2c;
+ --orange-soft: rgba(233, 138, 44, 0.16);
+ --red: #d95665;
+ --red-soft: rgba(217, 86, 101, 0.14);
+ --yellow: #d7a823;
+ --shadow: 0 24px 70px rgba(42, 86, 105, 0.2), 0 2px 10px rgba(43, 82, 98, 0.08);
+ --shadow-soft: 0 10px 32px rgba(39, 80, 96, 0.12);
+ --radius-xl: 28px;
+ --radius-lg: 21px;
+ --radius-md: 16px;
+ --blur: 26px;
+ font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+:root[data-theme="dark"] {
+ color-scheme: dark;
+ --page: #0d161d;
+ --page-deep: #172b39;
+ --glass: rgba(24, 38, 49, 0.68);
+ --glass-strong: rgba(28, 44, 56, 0.86);
+ --glass-soft: rgba(255, 255, 255, 0.045);
+ --line: rgba(193, 226, 240, 0.16);
+ --line-soft: rgba(188, 220, 234, 0.1);
+ --text: #eff8fb;
+ --muted: #9cb4bf;
+ --faint: #6e8793;
+ --blue: #56b9f0;
+ --blue-deep: #178ccf;
+ --blue-soft: rgba(69, 168, 225, 0.14);
+ --green: #52d394;
+ --green-soft: rgba(48, 193, 119, 0.14);
+ --orange: #f2a24c;
+ --orange-soft: rgba(242, 162, 76, 0.15);
+ --red: #ff7280;
+ --red-soft: rgba(255, 99, 116, 0.14);
+ --yellow: #f2c84b;
+ --shadow: 0 28px 80px rgba(0, 0, 0, 0.36), 0 2px 12px rgba(0, 0, 0, 0.28);
+ --shadow-soft: 0 12px 34px rgba(0, 0, 0, 0.22);
+}
+
+* { box-sizing: border-box; }
+
+html, body { min-height: 100%; }
+
+body {
+ margin: 0;
+ color: var(--text);
+ background:
+ radial-gradient(circle at 10% 5%, rgba(64, 176, 219, 0.32), transparent 32rem),
+ radial-gradient(circle at 88% 16%, rgba(73, 144, 225, 0.23), transparent 29rem),
+ radial-gradient(circle at 58% 92%, rgba(75, 191, 171, 0.2), transparent 32rem),
+ linear-gradient(145deg, var(--page), var(--page-deep));
+ background-attachment: fixed;
+ -webkit-font-smoothing: antialiased;
+}
+
+button, input { font: inherit; }
+button { color: inherit; }
+
+.ambient {
+ position: fixed;
+ inset: 0;
+ z-index: -1;
+ overflow: hidden;
+ pointer-events: none;
+}
+
+.ambient::before,
+.ambient::after {
+ content: "";
+ position: absolute;
+ width: 28rem;
+ height: 28rem;
+ border-radius: 50%;
+ filter: blur(34px);
+ opacity: 0.34;
+}
+
+.ambient::before { left: -10rem; top: -12rem; background: #62d7e3; }
+.ambient::after { right: -12rem; bottom: -15rem; background: #58a0ef; }
+
+.glass {
+ background: linear-gradient(145deg, var(--glass-strong), var(--glass));
+ border: 1px solid var(--line);
+ box-shadow: var(--shadow);
+ backdrop-filter: blur(var(--blur)) saturate(135%);
+ -webkit-backdrop-filter: blur(var(--blur)) saturate(135%);
+}
+
+.glass-inner {
+ background: linear-gradient(145deg, var(--glass-soft), rgba(255,255,255,0.015));
+ border: 1px solid var(--line-soft);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.08), var(--shadow-soft);
+}
+
+.eyebrow {
+ color: var(--muted);
+ font-size: 0.69rem;
+ font-weight: 750;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.brand { display: flex; align-items: center; gap: 0.78rem; min-width: 0; }
+.brand-mark {
+ display: grid;
+ place-items: center;
+ width: 2.65rem;
+ height: 2.65rem;
+ flex: 0 0 auto;
+ border-radius: 0.9rem;
+ color: white;
+ background: linear-gradient(145deg, #45b9ed, #147cba);
+ box-shadow: 0 8px 20px rgba(18, 126, 184, 0.26), inset 0 1px 0 rgba(255,255,255,0.35);
+}
+.brand-mark svg { width: 1.42rem; height: 1.42rem; }
+.brand-copy { min-width: 0; }
+.brand-title { margin: 0; font-size: 1.05rem; line-height: 1.15; letter-spacing: -0.015em; }
+.brand-subtitle { margin: 0.18rem 0 0; color: var(--muted); font-size: 0.72rem; }
+
+.pill {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.43rem;
+ min-height: 1.75rem;
+ padding: 0.3rem 0.66rem;
+ border: 1px solid transparent;
+ border-radius: 999px;
+ font-size: 0.68rem;
+ font-weight: 800;
+ letter-spacing: 0.055em;
+ white-space: nowrap;
+ text-transform: uppercase;
+}
+.pill::before { content: ""; width: 0.43rem; height: 0.43rem; border-radius: 50%; background: currentColor; box-shadow: 0 0 0 4px currentColor; opacity: 0.9; }
+.pill.good { color: var(--green); background: var(--green-soft); border-color: color-mix(in srgb, var(--green) 24%, transparent); }
+.pill.info { color: var(--blue); background: var(--blue-soft); border-color: color-mix(in srgb, var(--blue) 22%, transparent); }
+.pill.warn { color: var(--orange); background: var(--orange-soft); border-color: color-mix(in srgb, var(--orange) 24%, transparent); }
+.pill.bad { color: var(--red); background: var(--red-soft); border-color: color-mix(in srgb, var(--red) 24%, transparent); }
+.pill.muted { color: var(--muted); background: var(--glass-soft); border-color: var(--line-soft); }
+.pill.no-dot::before { display: none; }
+
+.icon-button, .soft-button, .primary-button, .step-button, .toggle, .segment {
+ border: 0;
+ cursor: pointer;
+ transition: transform 150ms ease, background 150ms ease, border-color 150ms ease, opacity 150ms ease, box-shadow 150ms ease;
+}
+.icon-button:hover, .soft-button:hover, .primary-button:hover, .step-button:hover, .toggle:hover, .segment:hover { transform: translateY(-1px); }
+.icon-button:active, .soft-button:active, .primary-button:active, .step-button:active, .toggle:active, .segment:active { transform: translateY(0) scale(0.98); }
+button:focus-visible, input:focus-visible { outline: 3px solid color-mix(in srgb, var(--blue) 32%, transparent); outline-offset: 2px; }
+button:disabled { cursor: wait; opacity: 0.55; transform: none !important; }
+
+.icon-button {
+ display: grid;
+ place-items: center;
+ width: 2.45rem;
+ height: 2.45rem;
+ border: 1px solid var(--line-soft);
+ border-radius: 0.82rem;
+ background: var(--glass-soft);
+}
+.icon-button svg { width: 1.12rem; height: 1.12rem; }
+
+.soft-button, .primary-button {
+ min-height: 2.55rem;
+ padding: 0.65rem 1rem;
+ border-radius: 0.88rem;
+ font-size: 0.77rem;
+ font-weight: 750;
+}
+.soft-button { color: var(--text); background: var(--glass-soft); border: 1px solid var(--line-soft); }
+.primary-button { color: white; background: linear-gradient(145deg, #35aae3, #157dbb); box-shadow: 0 9px 24px rgba(14, 119, 177, 0.25); }
+.soft-button.danger { color: var(--red); background: var(--red-soft); border-color: color-mix(in srgb, var(--red) 22%, transparent); }
+
+.stepper { display: flex; align-items: center; justify-content: center; gap: 0.5rem; }
+.step-button {
+ display: grid;
+ place-items: center;
+ width: 2.28rem;
+ height: 2.28rem;
+ border-radius: 0.78rem;
+ color: var(--blue);
+ background: var(--blue-soft);
+ border: 1px solid color-mix(in srgb, var(--blue) 22%, transparent);
+ font-size: 1.25rem;
+ font-weight: 500;
+ line-height: 1;
+}
+
+.toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.65rem;
+ min-width: 5.5rem;
+ padding: 0.48rem 0.58rem 0.48rem 0.7rem;
+ border: 1px solid var(--line-soft);
+ border-radius: 999px;
+ background: var(--glass-soft);
+ font-size: 0.68rem;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+}
+.toggle::after {
+ content: "";
+ width: 1.5rem;
+ height: 1.5rem;
+ border-radius: 50%;
+ background: var(--faint);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.3);
+}
+.toggle[aria-pressed="true"] { color: var(--green); background: var(--green-soft); border-color: color-mix(in srgb, var(--green) 26%, transparent); }
+.toggle[aria-pressed="true"]::after { background: var(--green); box-shadow: 0 0 14px color-mix(in srgb, var(--green) 52%, transparent); }
+.toggle.orange[aria-pressed="true"] { color: var(--orange); background: var(--orange-soft); border-color: color-mix(in srgb, var(--orange) 28%, transparent); }
+.toggle.orange[aria-pressed="true"]::after { background: var(--orange); box-shadow: 0 0 14px color-mix(in srgb, var(--orange) 48%, transparent); }
+
+.value { font-variant-numeric: tabular-nums; }
+.muted { color: var(--muted); }
+
+.range {
+ --progress: 50%;
+ width: 100%;
+ height: 1.35rem;
+ margin: 0;
+ appearance: none;
+ background: transparent;
+ cursor: pointer;
+}
+.range::-webkit-slider-runnable-track {
+ height: 0.42rem;
+ border-radius: 99px;
+ background: linear-gradient(90deg, var(--blue) var(--progress), var(--line-soft) var(--progress));
+}
+.range::-moz-range-track { height: 0.42rem; border-radius: 99px; background: var(--line-soft); }
+.range::-moz-range-progress { height: 0.42rem; border-radius: 99px; background: var(--blue); }
+.range::-webkit-slider-thumb {
+ width: 1.35rem;
+ height: 1.35rem;
+ margin-top: -0.465rem;
+ appearance: none;
+ border: 0.26rem solid var(--glass-strong);
+ border-radius: 50%;
+ background: var(--blue);
+ box-shadow: 0 3px 12px rgba(14, 118, 177, 0.36);
+}
+.range::-moz-range-thumb {
+ width: 0.92rem;
+ height: 0.92rem;
+ border: 0.25rem solid var(--glass-strong);
+ border-radius: 50%;
+ background: var(--blue);
+ box-shadow: 0 3px 12px rgba(14, 118, 177, 0.36);
+}
+
+.toast-region { position: fixed; right: 1rem; bottom: 1rem; z-index: 50; display: grid; gap: 0.55rem; }
+.toast {
+ max-width: min(23rem, calc(100vw - 2rem));
+ padding: 0.78rem 1rem;
+ border: 1px solid var(--line);
+ border-radius: 0.9rem;
+ background: var(--glass-strong);
+ box-shadow: var(--shadow-soft);
+ backdrop-filter: blur(18px);
+ font-size: 0.78rem;
+ animation: toast-in 180ms ease both;
+}
+.toast.error { color: var(--red); border-color: color-mix(in srgb, var(--red) 28%, transparent); }
+@keyframes toast-in { from { transform: translateY(0.55rem); opacity: 0; } }
+
+.skeleton { color: transparent !important; border-radius: 0.4rem; background: linear-gradient(100deg, var(--line-soft) 35%, var(--glass-soft) 50%, var(--line-soft) 65%); background-size: 200% 100%; animation: shimmer 1.3s infinite; }
+@keyframes shimmer { to { background-position-x: -200%; } }
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
+}
diff --git a/web/css/panel.css b/web/css/panel.css
new file mode 100644
index 0000000..7bbe5d7
--- /dev/null
+++ b/web/css/panel.css
@@ -0,0 +1,799 @@
+:root {
+ --panel-ink: #08265d;
+ --panel-muted: #315d91;
+ --tile: rgba(219, 241, 253, 0.22);
+ --tile-strong: rgba(244, 251, 255, 0.4);
+ --tile-line: rgba(255, 255, 255, 0.9);
+ --tile-line-soft: rgba(193, 226, 244, 0.5);
+ --tile-shadow: 0 14px 32px rgba(23, 75, 111, 0.16), inset 0 1px 0 rgba(255,255,255,0.92), inset 1px 0 0 rgba(255,255,255,0.36);
+ --panel-glass: rgba(211, 235, 248, 0.35);
+ --panel-edge: rgba(255, 255, 255, 0.92);
+ --blue-halo: rgba(25, 139, 255, 0.34);
+ --green-halo: rgba(30, 205, 120, 0.34);
+ --orange-halo: rgba(255, 126, 46, 0.3);
+}
+
+:root[data-theme="dark"] {
+ --panel-ink: #edf8ff;
+ --panel-muted: #9fc5df;
+ --tile: rgba(28, 48, 65, 0.46);
+ --tile-strong: rgba(45, 71, 89, 0.54);
+ --tile-line: rgba(194, 229, 247, 0.3);
+ --tile-line-soft: rgba(114, 176, 207, 0.22);
+ --tile-shadow: 0 16px 38px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(235,249,255,0.14), inset 1px 0 0 rgba(190,228,247,0.08);
+ --panel-glass: rgba(13, 29, 41, 0.58);
+ --panel-edge: rgba(187, 225, 244, 0.32);
+ --blue-halo: rgba(30, 161, 255, 0.42);
+ --green-halo: rgba(25, 225, 129, 0.35);
+ --orange-halo: rgba(255, 126, 39, 0.34);
+}
+
+html { min-width: 320px; }
+body {
+ min-height: 100vh;
+ padding: clamp(0.7rem, 2vw, 2rem);
+ color: var(--panel-ink);
+ background:
+ radial-gradient(ellipse at 8% 10%, rgba(244,253,255,.95) 0 7%, transparent 24%),
+ radial-gradient(ellipse at 73% -8%, rgba(255,249,239,.85) 0 10%, transparent 29%),
+ radial-gradient(ellipse at 93% 32%, rgba(90,152,125,.52) 0 10%, transparent 28%),
+ radial-gradient(ellipse at 12% 80%, rgba(65,132,170,.46) 0 12%, transparent 31%),
+ linear-gradient(115deg, rgba(184,222,239,.94), rgba(213,236,247,.82) 37%, rgba(239,232,217,.82) 66%, rgba(129,181,183,.88));
+ background-attachment: fixed;
+ overflow-x: hidden;
+ isolation: isolate;
+}
+
+:root[data-theme="dark"] body {
+ background:
+ radial-gradient(ellipse at 6% 2%, rgba(54,112,145,.44) 0 8%, transparent 28%),
+ radial-gradient(ellipse at 78% -5%, rgba(138,103,60,.31) 0 9%, transparent 28%),
+ radial-gradient(ellipse at 94% 36%, rgba(28,91,78,.44) 0 12%, transparent 31%),
+ radial-gradient(ellipse at 15% 87%, rgba(17,84,122,.42) 0 13%, transparent 33%),
+ linear-gradient(118deg, #081723, #122d3d 40%, #292c30 68%, #0c262c);
+}
+
+body::before {
+ content: "";
+ position: fixed;
+ inset: -7rem;
+ z-index: -3;
+ background:
+ linear-gradient(90deg, transparent 0 13%, rgba(255,255,255,.55) 13% 16%, transparent 16% 34%, rgba(255,255,255,.35) 34% 37%, transparent 37% 68%, rgba(255,255,255,.42) 68% 71%, transparent 71%),
+ linear-gradient(0deg, transparent 0 58%, rgba(228,244,251,.48) 58% 61%, transparent 61%),
+ radial-gradient(ellipse at 50% 114%, rgba(36,111,68,.48) 0 17%, transparent 47%);
+ filter: blur(26px);
+ transform: scale(1.05);
+}
+
+body::after {
+ content: "";
+ position: fixed;
+ inset: 0;
+ z-index: -2;
+ pointer-events: none;
+ background:
+ radial-gradient(circle at 4% 22%, rgba(255,255,255,.88) 0 .8%, transparent 5%),
+ radial-gradient(circle at 19% 74%, rgba(246,253,255,.68) 0 1.2%, transparent 7%),
+ radial-gradient(circle at 62% 34%, rgba(255,241,210,.63) 0 1.3%, transparent 8%),
+ radial-gradient(circle at 89% 13%, rgba(244,253,255,.66) 0 1%, transparent 6%),
+ radial-gradient(circle at 82% 82%, rgba(209,238,228,.48) 0 1.5%, transparent 8%);
+ filter: blur(8px);
+ opacity: .86;
+}
+
+.ambient::before { width: 42rem; height: 42rem; left: -14rem; top: -18rem; background: #c9f2ff; opacity: .58; filter: blur(54px); }
+.ambient::after { width: 46rem; height: 46rem; right: -14rem; bottom: -21rem; background: #ebd5a7; opacity: .38; filter: blur(64px); }
+:root[data-theme="dark"] .ambient::before { background: #147db5; opacity: .24; }
+:root[data-theme="dark"] .ambient::after { background: #846630; opacity: .2; }
+
+.climate-panel {
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+ width: min(100%, 104rem);
+ min-height: calc(100vh - clamp(1.4rem, 4vw, 4rem));
+ margin: 0 auto;
+ padding: clamp(0.65rem, 1.25vw, 1.25rem);
+ border: 1px solid var(--panel-edge);
+ outline: 1px solid rgba(255,255,255,.5);
+ outline-offset: -5px;
+ border-radius: 1.5rem;
+ background:
+ linear-gradient(125deg, rgba(255,255,255,.34), transparent 29%, rgba(255,255,255,.11) 67%, rgba(214,235,247,.2)),
+ var(--panel-glass);
+ box-shadow: 0 32px 90px rgba(20,60,89,.3), 0 2px 10px rgba(255,255,255,.35), inset 0 1px 0 rgba(255,255,255,.95), inset 0 -1px 0 rgba(114,162,192,.23);
+ backdrop-filter: blur(34px) saturate(148%);
+ -webkit-backdrop-filter: blur(34px) saturate(148%);
+}
+
+:root[data-theme="dark"] .climate-panel { outline-color: rgba(203,235,250,.12); box-shadow: 0 38px 100px rgba(0,0,0,.48), 0 0 42px rgba(21,120,171,.1), inset 0 1px 0 rgba(224,246,255,.2), inset 0 -1px 0 rgba(0,0,0,.35); }
+
+.climate-panel::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ border-radius: inherit;
+ background:
+ linear-gradient(112deg, rgba(255,255,255,.44) 0, rgba(255,255,255,.08) 17%, transparent 38%),
+ radial-gradient(ellipse at 52% -16%, rgba(255,255,255,.54), transparent 48%),
+ radial-gradient(ellipse at 100% 100%, rgba(102,178,219,.13), transparent 42%);
+}
+
+.climate-panel::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ opacity: .2;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
+ mix-blend-mode: soft-light;
+}
+
+.climate-panel > * { position: relative; z-index: 1; }
+
+.glass-tile, .glass-section {
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+ border: 1px solid var(--tile-line);
+ outline: 1px solid var(--tile-line-soft);
+ outline-offset: -3px;
+ background:
+ linear-gradient(132deg, rgba(255,255,255,.38), rgba(255,255,255,.06) 35%, transparent 63%),
+ linear-gradient(145deg, var(--tile-strong), var(--tile));
+ box-shadow: var(--tile-shadow);
+ backdrop-filter: blur(23px) saturate(145%);
+ -webkit-backdrop-filter: blur(23px) saturate(145%);
+}
+
+.glass-tile::before, .glass-section::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ border-radius: inherit;
+ background: linear-gradient(115deg, rgba(255,255,255,.35), transparent 25%, transparent 72%, rgba(255,255,255,.08));
+}
+.glass-tile > *, .glass-section > * { position: relative; z-index: 1; }
+
+.speed-card::after, .temperature-card::after, .power-card::after, .heater-card::after {
+ content: "";
+ position: absolute;
+ z-index: 0;
+ width: 13rem;
+ height: 13rem;
+ right: -5rem;
+ bottom: -7rem;
+ border-radius: 50%;
+ pointer-events: none;
+ filter: blur(17px);
+}
+.speed-card::after { background: var(--blue-halo); }
+.temperature-card::after { width: 18rem; background: linear-gradient(90deg, var(--blue-halo), var(--orange-halo)); }
+.power-card::after { background: var(--green-halo); opacity: .64; }
+.heater-card::after { background: var(--orange-halo); opacity: .58; }
+
+.panel-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0 0.65rem 0.7rem; }
+.identity, .header-tools { display: flex; align-items: center; gap: 0.65rem; }
+.identity h1 { margin: 0 0.35rem 0 0; font-size: clamp(1.8rem, 3.1vw, 3rem); line-height: 1; letter-spacing: -0.045em; }
+.climate-panel .pill { min-height: 2.05rem; padding: .38rem .82rem; border-color: rgba(255,255,255,.62); box-shadow: inset 0 1px 0 rgba(255,255,255,.72), 0 6px 18px rgba(26,75,108,.1); backdrop-filter: blur(14px); }
+.climate-panel .pill.good { box-shadow: 0 0 21px rgba(28,204,117,.18), inset 0 1px 0 rgba(255,255,255,.74); }
+.climate-panel .pill.warn { box-shadow: 0 0 21px rgba(242,145,56,.17), inset 0 1px 0 rgba(255,255,255,.74); }
+.climate-panel .pill.info { box-shadow: 0 0 21px rgba(31,145,236,.16), inset 0 1px 0 rgba(255,255,255,.74); }
+.climate-panel .icon-button { border-color: var(--tile-line); background: linear-gradient(145deg, var(--tile-strong), var(--tile)); box-shadow: var(--tile-shadow); backdrop-filter: blur(16px); }
+.header-tools { justify-content: flex-end; }
+.ventilation-badge { display: flex; align-items: center; gap: 0.7rem; min-width: 22rem; padding: 0.55rem 1rem; border-radius: 1rem; }
+.ventilation-badge svg { width: 2.5rem; color: #1977d2; filter: drop-shadow(0 0 9px rgba(26,128,226,.34)); }
+.ventilation-badge strong, .ventilation-badge small { display: block; }
+.ventilation-badge strong { font-size: 0.9rem; }
+.ventilation-badge small { margin-top: 0.1rem; color: var(--panel-muted); font-size: 0.68rem; }
+
+.primary-controls { display: grid; grid-template-columns: 2.05fr 2.05fr .95fr .95fr; gap: 0.75rem; }
+.control-card, .switch-card { min-height: 17.6rem; border-radius: 1.15rem; padding: 1rem; }
+.control-card { display: flex; flex-direction: column; }
+.card-title { display: flex; align-items: flex-start; gap: 0.72rem; }
+.card-title.compact { align-items: center; }
+.card-title h2 { margin: 0; font-size: clamp(1rem, 1.55vw, 1.28rem); letter-spacing: -0.02em; }
+.card-title p { margin: 0.18rem 0 0; color: var(--panel-muted); font-size: 0.72rem; }
+.feature-icon { display: grid; place-items: center; flex: 0 0 auto; width: 2.1rem; height: 2.1rem; color: #1389f4; font-size: 1.75rem; font-weight: 800; line-height: 1; text-shadow: 0 0 16px rgba(20,139,244,.46); }
+.feature-icon.green { color: #08a95a; text-shadow: 0 0 20px rgba(20,210,112,.62); }
+.feature-icon.orange { color: #f06419; text-shadow: 0 0 18px rgba(255,105,29,.46); }
+.fan-icon { font-size: 2.2rem; }
+.hero-control { display: grid; grid-template-columns: 4.3rem 1fr 4.3rem; align-items: center; gap: 1rem; margin: auto 0 0.75rem; }
+.hero-value { text-align: center; font-size: clamp(3.2rem, 5vw, 4.35rem); line-height: 1; letter-spacing: -0.06em; font-variant-numeric: tabular-nums; }
+.hero-step {
+ display: grid;
+ place-items: center;
+ width: 4.3rem;
+ height: 4.3rem;
+ border: 1.5px solid rgba(255,255,255,.85);
+ border-radius: 1rem;
+ color: #176dbb;
+ background: linear-gradient(145deg, rgba(245,252,255,.42), rgba(191,224,244,.2));
+ box-shadow: 0 10px 24px rgba(35,92,132,.15), inset 0 1px 0 rgba(255,255,255,.9), 0 0 0 1px rgba(185,226,248,.24);
+ cursor: pointer;
+ font-size: 2.3rem;
+ font-weight: 500;
+ transition: transform .15s ease, filter .15s ease;
+}
+.hero-step.primary { color: white; background: linear-gradient(145deg, #57b5ff, #1376e4 76%); box-shadow: 0 12px 28px rgba(16,103,220,.35), 0 0 26px var(--blue-halo), inset 0 1px 0 rgba(255,255,255,.8), inset 0 -2px 5px rgba(0,67,156,.24); }
+.hero-step:hover { transform: translateY(-2px); filter: brightness(1.08); box-shadow: 0 14px 32px rgba(25,112,219,.35), 0 0 32px var(--blue-halo), inset 0 1px 0 rgba(255,255,255,.78); }
+.hero-step:active { transform: scale(.98); }
+.speed-range { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); clip-path: inset(50%); white-space: nowrap; }
+.speed-segments { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.16rem; }
+.speed-segments button { height: 1.12rem; padding: 0; border: 1px solid rgba(255,255,255,.88); border-radius: .55rem; background: linear-gradient(180deg, rgba(173,207,229,.32), rgba(98,151,190,.2)); box-shadow: inset 0 1px 2px rgba(27,70,105,.08), 0 2px 5px rgba(32,86,126,.08); cursor: pointer; transition: background .12s ease, box-shadow .12s ease, transform .12s ease; }
+.speed-segments button:hover { transform: translateY(-1px); }
+.speed-segments button.active { background: linear-gradient(90deg, #0e82ed, #37b8ff); box-shadow: 0 0 17px rgba(25,143,247,.56), inset 0 1px 0 rgba(255,255,255,.74), inset 0 -2px 4px rgba(0,84,182,.22); }
+.speed-labels, .temperature-labels { display: flex; justify-content: space-between; margin-top: 0.45rem; padding: 0 .55rem; color: var(--panel-muted); font-size: 0.68rem; }
+.temperature-range { height: 1.45rem; }
+.temperature-range::-webkit-slider-runnable-track { height: .78rem; background: linear-gradient(90deg, #0878ed, #53c7f5 38%, #f3c26e 72%, #ff6b31); border: 1px solid rgba(255,255,255,.92); box-shadow: 0 0 16px rgba(37,153,239,.24), 0 0 14px rgba(255,114,41,.14), inset 0 1px 2px rgba(255,255,255,.5); }
+.temperature-range::-webkit-slider-thumb { width: 1.9rem; height: 1.9rem; margin-top: -.58rem; border-width: .28rem; box-shadow: 0 0 0 2px rgba(255,255,255,.55), 0 0 22px rgba(29,139,241,.58), 0 5px 13px rgba(17,79,143,.3); }
+.temperature-range::-moz-range-track { height: .78rem; background: linear-gradient(90deg, #0878ed, #53c7f5 38%, #f3c26e 72%, #ff6b31); }
+.temperature-range::-moz-range-progress { background: transparent; }
+.switch-card { display: flex; flex-direction: column; justify-content: space-between; }
+.glass-switch {
+ width: 100%; min-height: 6rem; border: 1.5px solid rgba(255,255,255,.9); border-radius: 1rem; color: var(--panel-ink); background: rgba(199,222,235,.28); box-shadow: var(--tile-shadow); cursor: pointer; font-size: 1.75rem; font-weight: 850;
+ text-shadow: 0 1px 0 rgba(255,255,255,.35);
+ transition: transform .15s ease, background .2s ease, box-shadow .2s ease, filter .2s ease;
+}
+.glass-switch:hover { transform: translateY(-2px); filter: brightness(1.06); }
+.glass-switch[aria-pressed="true"].green { background: linear-gradient(145deg, rgba(145,255,198,.9), rgba(38,194,113,.62)); border-color: rgba(213,255,232,.96); box-shadow: 0 14px 32px rgba(11,153,81,.3), 0 0 32px var(--green-halo), inset 0 1px 0 rgba(255,255,255,.9), inset 0 -4px 10px rgba(0,125,62,.18); }
+.glass-switch[aria-pressed="true"].orange { background: linear-gradient(145deg, rgba(255,202,130,.88), rgba(245,112,40,.58)); border-color: rgba(255,236,213,.95); box-shadow: 0 14px 32px rgba(210,91,25,.3), 0 0 30px var(--orange-halo), inset 0 1px 0 rgba(255,255,255,.88), inset 0 -4px 10px rgba(168,54,0,.17); }
+
+.content-section { margin-top: 0.75rem; padding: 0.7rem; border-radius: 1.15rem; }
+.section-title { display: flex; align-items: center; gap: 0.65rem; min-height: 2.35rem; padding: 0 0.35rem 0.5rem; }
+.section-title > div { display: flex; align-items: center; gap: 0.55rem; }
+.section-title h2 { margin: 0; font-size: clamp(1rem, 1.55vw, 1.28rem); }
+.section-icon { display: grid; place-items: center; width: 1.8rem; height: 1.8rem; color: #207bcc; font-size: 1.55rem; text-shadow: 0 0 14px rgba(26,124,210,.38); }
+.section-icon.leaf { color: #069c51; transform: rotate(-18deg); text-shadow: 0 0 15px rgba(8,173,88,.42); }
+.section-title .updated { margin-left: auto; color: var(--panel-muted); font-size: 0.7rem; }
+.air-grid { display: grid; grid-template-columns: 1.55fr 1fr .9fr 1fr 1fr 1.28fr; gap: 0.55rem; }
+.metric-card { min-width: 0; min-height: 9.4rem; padding: 0.85rem; border-radius: 1rem; }
+.metric-head, .metric-heading { display: flex; align-items: center; gap: 0.55rem; color: var(--panel-muted); font-size: 0.76rem; }
+.metric-head { justify-content: space-between; }
+.metric-icon { color: #137edc; font-size: 1.7rem; line-height: 1; text-shadow: 0 0 14px rgba(20,124,221,.34); }
+.metric-icon.dots { letter-spacing: -.2rem; }
+.metric-card > strong { display: block; margin-top: 0.75rem; font-size: clamp(1.55rem, 2.4vw, 2.2rem); line-height: 1; font-variant-numeric: tabular-nums; white-space: nowrap; }
+.metric-card strong small { color: var(--panel-muted); font-size: .63rem; font-weight: 600; }
+.quality-chip { display: inline-flex; justify-content: center; padding: .35rem .72rem; border: 1px solid rgba(109,237,167,.82); border-radius: 999px; color: #076d3d; background: linear-gradient(145deg, rgba(179,255,214,.7), rgba(92,218,151,.48)); box-shadow: 0 0 17px rgba(33,203,116,.24), inset 0 1px 0 rgba(255,255,255,.88); font-size: .66rem; font-weight: 750; }
+:root[data-theme="dark"] .quality-chip { color: #b9ffda; }
+.quality-chip.small { margin-top: .8rem; min-width: 5.5rem; }
+.co2-card > p { height: 1rem; margin: .2rem 0 .55rem; color: var(--panel-muted); font-size: .58rem; overflow: hidden; }
+.meter { height: .82rem; border: 1px solid rgba(255,255,255,.8); border-radius: 999px; background: rgba(91,145,181,.18); overflow: hidden; }
+.meter i { display: block; height: 100%; width: 0; border-radius: inherit; background: linear-gradient(90deg, #08a350, #36dc83); box-shadow: 0 0 18px rgba(18,199,104,.58), inset 0 1px 0 rgba(255,255,255,.64); transition: width .35s ease; }
+.meter-labels { display: flex; justify-content: space-between; margin-top: .25rem; color: var(--panel-muted); font-size: .55rem; }
+.filter-line { display: flex; align-items: center; gap: .45rem; margin-top: 1rem; }
+.filter-meter { flex: 1; }
+.filter-meter i { background: linear-gradient(90deg, #176dec, #36c1ff); box-shadow: 0 0 18px rgba(31,144,247,.55), inset 0 1px 0 rgba(255,255,255,.58); }
+.filter-line > span { font-size: .65rem; font-weight: 750; }
+
+.bottom-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 0.75rem; }
+.schedule-layout { display: grid; grid-template-columns: 1.35fr 1fr; gap: 0.7rem; }
+.schedule-times { display: grid; grid-template-columns: 1fr 1px 1fr; gap: 1rem; align-items: center; min-height: 8.2rem; padding: 1rem 1.3rem; border-radius: 1rem; }
+.schedule-times > i { width: 1px; height: 80%; background: rgba(49,93,145,.25); }
+.schedule-times div { display: grid; gap: .22rem; }
+.schedule-times span, .schedule-times small { color: var(--panel-muted); font-size: .68rem; }
+.schedule-times strong { font-size: clamp(1.65rem, 2.8vw, 2.3rem); font-variant-numeric: tabular-nums; }
+.schedule-buttons { display: grid; gap: .55rem; }
+.schedule-button { display: grid; grid-template-columns: 2.4rem 1fr; align-items: center; gap: .65rem; min-height: 3.8rem; padding: .55rem .8rem; border: 1.5px solid; border-radius: .9rem; text-align: left; cursor: pointer; }
+.schedule-button strong, .schedule-button small { display: block; }
+.schedule-button strong { font-size: .84rem; }
+.schedule-button small { margin-top: .12rem; font-size: .62rem; }
+.schedule-button.auto { color: #096bda; border-color: rgba(83,176,255,.9); background: linear-gradient(145deg, rgba(224,244,255,.67), rgba(133,201,250,.26)); box-shadow: 0 9px 24px rgba(23,113,212,.19), 0 0 20px rgba(42,153,243,.13), inset 0 1px 0 rgba(255,255,255,.92); }
+.schedule-button.pause { color: #a73512; border-color: rgba(250,174,102,.9); background: linear-gradient(145deg, rgba(255,240,217,.72), rgba(246,177,107,.28)); box-shadow: 0 9px 24px rgba(202,104,44,.17), 0 0 20px rgba(244,131,50,.12), inset 0 1px 0 rgba(255,255,255,.92); }
+.schedule-button:hover { filter: brightness(1.06); transform: translateY(-1px); }
+.action-symbol { font-size: 2rem; font-weight: 800; text-align: center; }
+.mode-banner { display: flex; gap: .4rem; align-items: center; margin-top: .5rem; padding: .45rem .7rem; border-radius: .65rem; color: var(--panel-muted); background: rgba(255,255,255,.14); font-size: .62rem; }
+.mode-banner small::before { content: "· "; }
+.extras-grid { display: grid; grid-template-columns: .9fr 1.3fr; gap: .55rem; }
+.extra-card { display: flex; align-items: center; gap: .8rem; min-height: 8.2rem; padding: 1rem; border-radius: 1rem; }
+.extra-icon { color: #267fd3; font-size: 3.4rem; line-height: 1; text-shadow: 0 0 19px rgba(33,126,211,.32); }
+.extra-icon.wind { font-size: 3.8rem; transform: rotate(90deg); }
+.extra-card small, .extra-card strong, .extra-card em { display: block; }
+.extra-card small { color: var(--panel-muted); font-size: .7rem; }
+.extra-card strong { margin-top: .18rem; font-size: clamp(1.3rem, 2.2vw, 2rem); font-style: normal; }
+.extra-card em { margin-top: .18rem; color: var(--panel-muted); font-size: .62rem; font-style: normal; }
+.mode-card { display: grid; grid-template-columns: auto 1fr; }
+.mode-card strong { font-size: .85rem; }
+.segments { grid-column: 1 / -1; display: flex; gap: .3rem; }
+.segment { flex: 1; padding: .42rem; border: 1px solid rgba(255,255,255,.55); border-radius: .55rem; color: var(--panel-muted); background: rgba(255,255,255,.14); cursor: pointer; font-size: .58rem; }
+.segment.active { color: white; background: linear-gradient(145deg, #45a7f4, #1976d2); }
+.micro-settings { display: flex; align-items: center; gap: .65rem; margin-top: .55rem; padding: 0 .25rem; color: var(--panel-muted); font-size: .58rem; }
+.micro-settings > span:nth-child(2) { margin-right: auto; }
+.micro-toggle { padding: .32rem .55rem; border: 1px solid var(--tile-line); border-radius: 999px; color: var(--panel-muted); background: var(--tile); cursor: pointer; font-size: .56rem; }
+.micro-toggle[aria-pressed="true"] { color: #087848; background: rgba(105,222,160,.38); }
+.demo-note { display: block; margin-top: .55rem; color: var(--panel-muted); text-align: center; font-size: .62rem; }
+
+/* Dark aeroglass keeps the same optical depth, with colder edges and
+ brighter local halos so active controls do not disappear into the glass. */
+:root[data-theme="dark"] body::after { opacity: .42; mix-blend-mode: screen; }
+:root[data-theme="dark"] .climate-panel::before {
+ background:
+ linear-gradient(112deg, rgba(205,239,255,.14), rgba(255,255,255,.025) 21%, transparent 42%),
+ radial-gradient(ellipse at 52% -16%, rgba(112,202,244,.13), transparent 50%),
+ radial-gradient(ellipse at 100% 100%, rgba(23,139,186,.12), transparent 44%);
+}
+:root[data-theme="dark"] .glass-tile,
+:root[data-theme="dark"] .glass-section {
+ background:
+ linear-gradient(132deg, rgba(211,240,253,.105), rgba(255,255,255,.018) 38%, transparent 65%),
+ linear-gradient(145deg, var(--tile-strong), var(--tile));
+}
+:root[data-theme="dark"] .glass-tile::before,
+:root[data-theme="dark"] .glass-section::before {
+ background: linear-gradient(115deg, rgba(218,243,255,.12), transparent 26%, transparent 74%, rgba(80,181,222,.045));
+}
+:root[data-theme="dark"] .hero-step {
+ color: #8ed3ff;
+ border-color: rgba(184,226,247,.28);
+ background: linear-gradient(145deg, rgba(89,137,166,.2), rgba(20,48,67,.23));
+ box-shadow: 0 12px 28px rgba(0,0,0,.28), inset 0 1px 0 rgba(224,246,255,.14), 0 0 0 1px rgba(60,157,205,.08);
+}
+:root[data-theme="dark"] .hero-step.primary {
+ color: white;
+ background: linear-gradient(145deg, #42b8ff, #096bd3 78%);
+ box-shadow: 0 13px 32px rgba(0,65,139,.5), 0 0 30px var(--blue-halo), inset 0 1px 0 rgba(220,248,255,.52), inset 0 -3px 7px rgba(0,27,91,.38);
+}
+:root[data-theme="dark"] .speed-segments button { border-color: rgba(192,226,244,.25); background: linear-gradient(180deg, rgba(91,136,162,.23), rgba(24,55,74,.28)); }
+:root[data-theme="dark"] .speed-segments button.active { border-color: rgba(117,207,255,.55); background: linear-gradient(90deg, #0877df, #20b7ff); box-shadow: 0 0 20px rgba(24,156,255,.66), inset 0 1px 0 rgba(223,248,255,.42); }
+:root[data-theme="dark"] .glass-switch { color: #ddecf4; border-color: rgba(193,228,245,.25); background: linear-gradient(145deg, rgba(77,112,133,.23), rgba(20,44,59,.28)); text-shadow: 0 1px 2px rgba(0,0,0,.4); }
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].green { color: #eafff4; border-color: rgba(118,255,185,.54); background: linear-gradient(145deg, rgba(56,221,137,.76), rgba(8,120,69,.67)); box-shadow: 0 15px 36px rgba(0,0,0,.3), 0 0 38px var(--green-halo), inset 0 1px 0 rgba(218,255,236,.48); }
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].orange { color: #fff5e9; border-color: rgba(255,185,118,.53); background: linear-gradient(145deg, rgba(244,145,61,.76), rgba(143,58,14,.68)); box-shadow: 0 15px 36px rgba(0,0,0,.3), 0 0 36px var(--orange-halo), inset 0 1px 0 rgba(255,237,216,.48); }
+:root[data-theme="dark"] .quality-chip { color: #caffdf; border-color: rgba(89,232,158,.42); background: linear-gradient(145deg, rgba(45,186,113,.35), rgba(11,101,60,.3)); box-shadow: 0 0 20px rgba(20,218,118,.18), inset 0 1px 0 rgba(210,255,232,.16); }
+:root[data-theme="dark"] .schedule-button.auto { color: #a8ddff; border-color: rgba(76,176,241,.48); background: linear-gradient(145deg, rgba(38,125,187,.29), rgba(15,61,91,.28)); box-shadow: 0 10px 27px rgba(0,0,0,.23), 0 0 24px rgba(27,150,237,.13), inset 0 1px 0 rgba(211,241,255,.13); }
+:root[data-theme="dark"] .schedule-button.pause { color: #ffc18c; border-color: rgba(242,147,70,.48); background: linear-gradient(145deg, rgba(167,82,27,.3), rgba(73,40,25,.27)); box-shadow: 0 10px 27px rgba(0,0,0,.23), 0 0 24px rgba(244,117,35,.12), inset 0 1px 0 rgba(255,230,207,.12); }
+:root[data-theme="dark"] .meter { border-color: rgba(200,232,247,.24); background: rgba(3,22,34,.34); box-shadow: inset 0 2px 5px rgba(0,0,0,.2); }
+:root[data-theme="dark"] .micro-toggle { border-color: rgba(190,226,244,.2); background: rgba(76,117,141,.15); }
+:root[data-theme="dark"] .micro-toggle[aria-pressed="true"] { color: #b8ffda; border-color: rgba(67,213,139,.35); background: rgba(21,149,85,.26); box-shadow: 0 0 15px rgba(20,208,114,.12); }
+
+/* Photographic aeroglass refinement. The backdrop already contains optical
+ blur, so the UI only adds a restrained haze instead of whitening the scene. */
+:root {
+ --tile: rgba(214, 238, 251, 0.14);
+ --tile-strong: rgba(247, 252, 255, 0.27);
+ --tile-line: rgba(255, 255, 255, 0.82);
+ --tile-line-soft: rgba(206, 235, 249, 0.43);
+ --panel-glass: rgba(200, 228, 244, 0.2);
+ --blue-halo: rgba(23, 137, 248, 0.16);
+ --green-halo: rgba(28, 196, 113, 0.16);
+ --orange-halo: rgba(246, 126, 50, 0.14);
+}
+:root[data-theme="dark"] {
+ --tile: rgba(16, 37, 51, 0.28);
+ --tile-strong: rgba(52, 78, 96, 0.31);
+ --tile-line: rgba(203, 234, 248, 0.25);
+ --tile-line-soft: rgba(114, 176, 207, 0.16);
+ --panel-glass: rgba(7, 22, 33, 0.48);
+ --blue-halo: rgba(29, 153, 245, 0.22);
+ --green-halo: rgba(27, 210, 121, 0.19);
+ --orange-halo: rgba(246, 125, 46, 0.18);
+}
+body {
+ background-color: #b7d5e3;
+ background-image:
+ linear-gradient(118deg, rgba(180,220,239,.16), rgba(238,247,251,.04) 45%, rgba(247,226,204,.1)),
+ url("../assets/mountain-lake-light.jpg");
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+}
+:root[data-theme="dark"] body {
+ background-color: #091722;
+ background-image:
+ linear-gradient(118deg, rgba(2,15,25,.66), rgba(8,27,40,.57) 48%, rgba(20,25,29,.64)),
+ url("../assets/mountain-lake-dark.jpg");
+ background-position: center;
+ background-size: cover;
+}
+body::before {
+ inset: 0;
+ background: linear-gradient(110deg, rgba(214,242,253,.16), transparent 35%, rgba(255,240,218,.1) 76%, transparent);
+ filter: none;
+ transform: none;
+ opacity: .75;
+}
+body::after {
+ background:
+ radial-gradient(circle at 11% 18%, rgba(255,255,255,.33) 0 .5%, transparent 5%),
+ radial-gradient(circle at 64% 23%, rgba(255,244,218,.28) 0 .6%, transparent 6%),
+ radial-gradient(circle at 91% 79%, rgba(225,250,237,.22) 0 .8%, transparent 7%);
+ filter: blur(11px);
+ opacity: .5;
+}
+:root[data-theme="dark"] body::before { background: linear-gradient(115deg, rgba(29,104,142,.1), transparent 40%, rgba(110,76,42,.08)); opacity: .7; }
+:root[data-theme="dark"] body::after { opacity: .26; }
+.ambient::before { opacity: .14; filter: blur(76px); }
+.ambient::after { opacity: .11; filter: blur(82px); }
+:root[data-theme="dark"] .ambient::before { opacity: .12; }
+:root[data-theme="dark"] .ambient::after { opacity: .08; }
+.climate-panel {
+ background:
+ linear-gradient(125deg, rgba(255,255,255,.22), transparent 31%, rgba(255,255,255,.055) 69%, rgba(196,224,240,.1)),
+ var(--panel-glass);
+ box-shadow: 0 27px 68px rgba(22,66,95,.24), 0 2px 8px rgba(255,255,255,.28), inset 0 1px 0 rgba(255,255,255,.88), inset 0 -1px 0 rgba(113,163,192,.17);
+ backdrop-filter: blur(13px) saturate(124%);
+ -webkit-backdrop-filter: blur(13px) saturate(124%);
+}
+.climate-panel::before {
+ background:
+ linear-gradient(112deg, rgba(255,255,255,.25), rgba(255,255,255,.035) 18%, transparent 39%),
+ radial-gradient(ellipse at 50% -20%, rgba(255,255,255,.25), transparent 47%);
+}
+.climate-panel::after { opacity: .1; }
+:root[data-theme="dark"] .climate-panel { box-shadow: 0 33px 78px rgba(0,0,0,.43), 0 0 28px rgba(19,112,158,.06), inset 0 1px 0 rgba(221,243,252,.17), inset 0 -1px 0 rgba(0,0,0,.28); }
+:root[data-theme="dark"] .climate-panel::before { background: linear-gradient(112deg, rgba(203,238,253,.1), transparent 24%, transparent 72%, rgba(71,163,202,.035)); }
+.glass-tile, .glass-section {
+ background:
+ linear-gradient(132deg, rgba(255,255,255,.25), rgba(255,255,255,.035) 35%, transparent 64%),
+ linear-gradient(145deg, var(--tile-strong), var(--tile));
+ box-shadow: 0 10px 25px rgba(24,72,105,.12), inset 0 1px 0 rgba(255,255,255,.78), inset 1px 0 0 rgba(255,255,255,.24);
+ backdrop-filter: blur(16px) saturate(127%);
+ -webkit-backdrop-filter: blur(16px) saturate(127%);
+}
+.glass-tile::before, .glass-section::before { opacity: .62; }
+:root[data-theme="dark"] .glass-tile,
+:root[data-theme="dark"] .glass-section {
+ background:
+ linear-gradient(132deg, rgba(213,240,252,.075), rgba(255,255,255,.012) 38%, transparent 65%),
+ linear-gradient(145deg, var(--tile-strong), var(--tile));
+ box-shadow: 0 13px 29px rgba(0,0,0,.26), inset 0 1px 0 rgba(226,246,255,.11), inset 1px 0 0 rgba(186,226,245,.055);
+}
+.speed-card::after, .temperature-card::after, .power-card::after, .heater-card::after { filter: blur(32px); opacity: .48; }
+.power-card::after { opacity: .38; }
+.heater-card::after { opacity: .34; }
+.hero-step.primary { box-shadow: 0 10px 23px rgba(16,103,220,.25), 0 0 15px var(--blue-halo), inset 0 1px 0 rgba(255,255,255,.76), inset 0 -2px 5px rgba(0,67,156,.2); }
+.hero-step:hover { box-shadow: 0 12px 27px rgba(25,112,219,.27), 0 0 19px var(--blue-halo), inset 0 1px 0 rgba(255,255,255,.75); }
+.speed-segments button.active { box-shadow: 0 0 9px rgba(25,143,247,.34), inset 0 1px 0 rgba(255,255,255,.66), inset 0 -2px 4px rgba(0,84,182,.18); }
+.temperature-range::-webkit-slider-runnable-track { box-shadow: 0 0 8px rgba(37,153,239,.15), 0 0 7px rgba(255,114,41,.09), inset 0 1px 2px rgba(255,255,255,.46); }
+.temperature-range::-webkit-slider-thumb { box-shadow: 0 0 0 2px rgba(255,255,255,.48), 0 0 12px rgba(29,139,241,.34), 0 4px 10px rgba(17,79,143,.24); }
+.glass-switch[aria-pressed="true"].green { box-shadow: 0 12px 27px rgba(11,153,81,.22), 0 0 19px var(--green-halo), inset 0 1px 0 rgba(255,255,255,.84), inset 0 -4px 10px rgba(0,125,62,.14); }
+.glass-switch[aria-pressed="true"].orange { box-shadow: 0 12px 27px rgba(210,91,25,.2), 0 0 18px var(--orange-halo), inset 0 1px 0 rgba(255,255,255,.83), inset 0 -4px 10px rgba(168,54,0,.13); }
+.quality-chip { box-shadow: 0 0 10px rgba(33,203,116,.13), inset 0 1px 0 rgba(255,255,255,.83); }
+.meter i { box-shadow: 0 0 10px rgba(18,199,104,.35), inset 0 1px 0 rgba(255,255,255,.58); }
+.filter-meter i { box-shadow: 0 0 10px rgba(31,144,247,.32), inset 0 1px 0 rgba(255,255,255,.54); }
+.schedule-button.auto, .schedule-button.pause { box-shadow: 0 7px 18px rgba(31,83,119,.12), inset 0 1px 0 rgba(255,255,255,.82); }
+:root[data-theme="dark"] .hero-step.primary { box-shadow: 0 11px 27px rgba(0,65,139,.4), 0 0 18px var(--blue-halo), inset 0 1px 0 rgba(220,248,255,.42), inset 0 -3px 7px rgba(0,27,91,.3); }
+:root[data-theme="dark"] .speed-segments button.active { box-shadow: 0 0 12px rgba(24,156,255,.4), inset 0 1px 0 rgba(223,248,255,.36); }
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].green { box-shadow: 0 12px 28px rgba(0,0,0,.28), 0 0 20px var(--green-halo), inset 0 1px 0 rgba(218,255,236,.4); }
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].orange { box-shadow: 0 12px 28px rgba(0,0,0,.28), 0 0 20px var(--orange-halo), inset 0 1px 0 rgba(255,237,216,.4); }
+
+/* Natural highlights: state colors tint the glass, but do not paint opaque
+ neon blocks over the photographic background. Heater ON is a healthy green
+ state too; its orange icon still communicates heat. */
+.speed-card::after, .temperature-card::after { opacity: .18; filter: blur(42px); }
+.power-card::after, .heater-card::after { background: var(--green-halo); opacity: .16; filter: blur(44px); }
+.hero-step.primary { box-shadow: 0 9px 20px rgba(15,92,183,.22), 0 0 9px rgba(48,151,239,.14), inset 0 1px 0 rgba(255,255,255,.78), inset 0 -2px 5px rgba(0,67,156,.18); }
+.hero-step:hover { box-shadow: 0 11px 23px rgba(20,101,190,.24), 0 0 12px rgba(48,151,239,.16), inset 0 1px 0 rgba(255,255,255,.76); }
+.speed-segments button.active { box-shadow: 0 0 6px rgba(25,143,247,.24), inset 0 1px 0 rgba(255,255,255,.68), inset 0 -2px 4px rgba(0,84,182,.16); }
+.glass-switch[aria-pressed="true"].green,
+.glass-switch[aria-pressed="true"].orange {
+ background: linear-gradient(145deg, rgba(139,244,190,.72), rgba(55,194,122,.48));
+ border-color: rgba(218,255,235,.84);
+ box-shadow: 0 10px 23px rgba(18,143,80,.18), 0 0 11px rgba(38,199,116,.13), inset 0 1px 0 rgba(255,255,255,.82), inset 0 -3px 8px rgba(0,114,57,.1);
+}
+:root[data-theme="dark"] .speed-card::after,
+:root[data-theme="dark"] .temperature-card::after { opacity: .2; }
+:root[data-theme="dark"] .power-card::after,
+:root[data-theme="dark"] .heater-card::after { opacity: .18; }
+:root[data-theme="dark"] .hero-step.primary { box-shadow: 0 10px 24px rgba(0,53,119,.38), 0 0 11px rgba(41,162,244,.19), inset 0 1px 0 rgba(220,248,255,.4), inset 0 -3px 7px rgba(0,27,91,.27); }
+:root[data-theme="dark"] .speed-segments button.active { box-shadow: 0 0 7px rgba(24,156,255,.27), inset 0 1px 0 rgba(223,248,255,.34); }
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].green,
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].orange {
+ color: #effff6;
+ border-color: rgba(127,242,182,.4);
+ background: linear-gradient(145deg, rgba(51,190,119,.58), rgba(9,102,59,.55));
+ box-shadow: 0 11px 25px rgba(0,0,0,.25), 0 0 12px rgba(37,207,121,.15), inset 0 1px 0 rgba(218,255,236,.34);
+}
+
+/* Final material and icon polish. */
+.ui-icon {
+ display: block;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+.feature-icon { padding: .08rem; filter: drop-shadow(0 0 6px rgba(20,139,244,.2)); text-shadow: none; }
+.feature-icon.green { filter: drop-shadow(0 0 6px rgba(20,190,104,.24)); }
+.feature-icon.orange { filter: drop-shadow(0 0 6px rgba(244,105,31,.22)); }
+.fan-icon { font-size: inherit; }
+.section-icon { padding: .06rem; filter: drop-shadow(0 0 5px rgba(26,124,210,.17)); text-shadow: none; }
+.section-icon.leaf { filter: drop-shadow(0 0 5px rgba(8,160,81,.2)); }
+.metric-icon { width: 1.7rem; height: 1.7rem; flex: 0 0 auto; filter: drop-shadow(0 0 4px rgba(20,124,221,.15)); text-shadow: none; }
+.metric-icon.particles { padding: .05rem; }
+.action-symbol { width: 2rem; height: 2rem; font-size: inherit; }
+.extra-icon { width: 3.4rem; height: 3.4rem; flex: 0 0 auto; filter: drop-shadow(0 0 7px rgba(33,126,211,.17)); text-shadow: none; }
+.extra-icon.wind { width: 3.8rem; height: 3.8rem; font-size: inherit; transform: none; }
+
+:root[data-theme="light"] {
+ --tile: rgba(104, 126, 139, 0.16);
+ --tile-strong: rgba(214, 226, 232, 0.2);
+ --tile-line-soft: rgba(189, 211, 221, 0.38);
+}
+:root[data-theme="light"] .glass-tile,
+:root[data-theme="light"] .glass-section {
+ background:
+ linear-gradient(132deg, rgba(244,248,250,.22), rgba(159,178,188,.055) 38%, transparent 66%),
+ linear-gradient(145deg, var(--tile-strong), var(--tile));
+ backdrop-filter: blur(14px) saturate(116%);
+ -webkit-backdrop-filter: blur(14px) saturate(116%);
+}
+
+.metric-card:not(.co2-card) {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+}
+.metric-card:not(.co2-card) .metric-heading { width: 100%; justify-content: center; text-align: left; }
+.metric-card:not(.co2-card) > strong { margin-top: .7rem; }
+.metric-card:not(.co2-card) .quality-chip { align-self: center; }
+.filter-card .filter-line { width: 100%; }
+
+.hero-step.primary {
+ background:
+ radial-gradient(circle at 24% 5%, rgba(255,255,255,.55), transparent 38%),
+ linear-gradient(135deg, rgba(255,255,255,.16), transparent 42%, rgba(0,73,170,.1)),
+ linear-gradient(145deg, #55b4ff, #1479e8 72%);
+ box-shadow: 0 8px 18px rgba(15,92,183,.18), 0 0 7px rgba(48,151,239,.1), inset 0 1px 0 rgba(255,255,255,.86), inset 0 -3px 7px rgba(0,67,156,.16);
+}
+.hero-step.primary:hover { box-shadow: 0 10px 21px rgba(20,101,190,.21), 0 0 9px rgba(48,151,239,.12), inset 0 1px 0 rgba(255,255,255,.88); }
+.speed-segments button.active {
+ background:
+ linear-gradient(180deg, rgba(255,255,255,.32), transparent 48%),
+ linear-gradient(90deg, #1685ec, #35afff);
+ box-shadow: 0 0 5px rgba(25,143,247,.19), inset 0 1px 0 rgba(255,255,255,.75), inset 0 -2px 4px rgba(0,84,182,.13);
+}
+.glass-switch { overflow: hidden; }
+.glass-switch[aria-pressed="true"].green,
+.glass-switch[aria-pressed="true"].orange {
+ background:
+ radial-gradient(circle at 25% -8%, rgba(255,255,255,.67), transparent 42%),
+ linear-gradient(132deg, rgba(255,255,255,.15), transparent 40%, rgba(0,125,62,.06)),
+ linear-gradient(145deg, rgba(139,244,190,.67), rgba(55,194,122,.43));
+ box-shadow: 0 8px 19px rgba(18,143,80,.14), 0 0 8px rgba(38,199,116,.09), inset 0 1px 0 rgba(255,255,255,.9), inset 0 -4px 9px rgba(0,114,57,.08);
+}
+.schedule-button.auto {
+ background:
+ radial-gradient(circle at 19% -30%, rgba(255,255,255,.55), transparent 44%),
+ linear-gradient(135deg, rgba(255,255,255,.13), transparent 43%),
+ linear-gradient(145deg, rgba(211,239,255,.52), rgba(100,179,235,.18));
+}
+.schedule-button.pause {
+ background:
+ radial-gradient(circle at 19% -30%, rgba(255,255,255,.58), transparent 44%),
+ linear-gradient(135deg, rgba(255,255,255,.13), transparent 43%),
+ linear-gradient(145deg, rgba(255,231,199,.55), rgba(235,151,76,.18));
+}
+:root[data-theme="dark"] .hero-step.primary {
+ background:
+ radial-gradient(circle at 24% 5%, rgba(214,246,255,.32), transparent 38%),
+ linear-gradient(135deg, rgba(255,255,255,.08), transparent 42%, rgba(0,32,95,.17)),
+ linear-gradient(145deg, #33aaf4, #0869d0 74%);
+}
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].green,
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].orange {
+ background:
+ radial-gradient(circle at 25% -8%, rgba(218,255,236,.3), transparent 42%),
+ linear-gradient(132deg, rgba(255,255,255,.07), transparent 40%, rgba(0,45,23,.12)),
+ linear-gradient(145deg, rgba(51,190,119,.52), rgba(9,102,59,.49));
+ box-shadow: 0 9px 21px rgba(0,0,0,.22), 0 0 9px rgba(37,207,121,.1), inset 0 1px 0 rgba(218,255,236,.32), inset 0 -3px 8px rgba(0,31,16,.16);
+}
+:root[data-theme="dark"] .schedule-button.auto {
+ background:
+ radial-gradient(circle at 19% -30%, rgba(208,243,255,.17), transparent 45%),
+ linear-gradient(145deg, rgba(38,125,187,.26), rgba(15,61,91,.25));
+}
+:root[data-theme="dark"] .schedule-button.pause {
+ background:
+ radial-gradient(circle at 19% -30%, rgba(255,226,199,.16), transparent 45%),
+ linear-gradient(145deg, rgba(167,82,27,.27), rgba(73,40,25,.25));
+}
+
+@media (min-width: 1300px) and (min-height: 760px) {
+ .climate-panel {
+ width: 100%;
+ max-width: none;
+ display: grid;
+ grid-template-rows: auto minmax(18rem, 1.4fr) minmax(11rem, .78fr) minmax(13rem, .9fr) auto;
+ gap: .75rem;
+ }
+ .panel-header { padding-bottom: 0; }
+ .primary-controls, .bottom-grid { min-height: 0; height: 100%; }
+ .control-card, .switch-card { min-height: 0; height: 100%; }
+ .content-section { min-height: 0; height: 100%; margin-top: 0; display: flex; flex-direction: column; }
+ .air-grid { min-height: 0; flex: 1; }
+ .metric-card { min-height: 0; height: 100%; }
+ .schedule-layout, .extras-grid { min-height: 0; flex: 1; }
+ .schedule-times, .extra-card { min-height: 0; height: 100%; }
+}
+
+@media (min-width: 1700px) {
+ html { font-size: 18px; }
+ .climate-panel { padding: 1.2rem; }
+}
+
+@media (max-width: 1120px) {
+ .primary-controls { grid-template-columns: 1fr 1fr; }
+ .switch-card { min-height: 9rem; }
+ .glass-switch { min-height: 3.8rem; }
+ .air-grid { grid-template-columns: repeat(3, 1fr); }
+ .bottom-grid { grid-template-columns: 1fr; }
+}
+
+@media (max-width: 760px) {
+ body { padding: .5rem; }
+ .climate-panel { border-radius: 1.1rem; }
+ .panel-header, .identity, .header-tools { flex-wrap: wrap; }
+ .panel-header { align-items: flex-start; }
+ .header-tools { width: 100%; justify-content: flex-start; }
+ .ventilation-badge { min-width: 0; flex: 1; }
+ .primary-controls { grid-template-columns: 1fr; }
+ .control-card, .switch-card { min-height: auto; }
+ .hero-control { margin: 1.4rem 0 .75rem; }
+ .air-grid { grid-template-columns: 1fr 1fr; }
+ .co2-card, .filter-card { grid-column: 1 / -1; }
+ .schedule-layout { grid-template-columns: 1fr; }
+}
+
+@media (max-width: 480px) {
+ .identity h1 { width: 100%; }
+ .ventilation-badge small { display: none; }
+ .air-grid, .extras-grid { grid-template-columns: 1fr; }
+ .co2-card, .filter-card { grid-column: auto; }
+ .schedule-times { grid-template-columns: 1fr; }
+ .schedule-times > i { width: 100%; height: 1px; }
+ .micro-settings { flex-wrap: wrap; }
+}
+
+@media (prefers-reduced-motion: reduce) { .hero-step:hover { transform: none; } }
+
+/* Schedule controls and editor. */
+.schedule-title-tools { margin-left: auto; }
+.schedule-edit-button {
+ display: inline-flex;
+ align-items: center;
+ gap: .4rem;
+ min-height: 2.05rem;
+ padding: .36rem .68rem;
+ border: 1px solid var(--tile-line);
+ border-radius: .7rem;
+ color: #126fc4;
+ background:
+ radial-gradient(circle at 20% -35%, rgba(255,255,255,.48), transparent 48%),
+ linear-gradient(145deg, rgba(220,241,253,.42), rgba(100,173,220,.13));
+ box-shadow: 0 6px 15px rgba(27,91,135,.1), inset 0 1px 0 rgba(255,255,255,.76);
+ cursor: pointer;
+ font-size: .65rem;
+ font-weight: 750;
+ transition: transform .15s ease, filter .15s ease;
+}
+.schedule-edit-button svg { width: 1rem; height: 1rem; }
+.schedule-edit-button:hover { transform: translateY(-1px); filter: brightness(1.06); }
+.schedule-button:disabled { cursor: not-allowed; opacity: .5; filter: saturate(.38); box-shadow: none; }
+.schedule-button:disabled:hover { transform: none; filter: saturate(.38); }
+.schedule-button[aria-busy="true"] { cursor: wait; }
+
+#schedule-state.warn {
+ color: #8d3b00;
+ border-color: rgba(247,162,73,.78);
+ background: linear-gradient(145deg, rgba(255,232,190,.82), rgba(240,157,69,.42));
+ box-shadow: 0 0 12px rgba(237,130,39,.13), inset 0 1px 0 rgba(255,255,255,.86);
+ font-size: .66rem;
+ letter-spacing: .07em;
+}
+:root[data-theme="dark"] #schedule-state.warn {
+ color: #ffe2b8;
+ border-color: rgba(255,166,83,.57);
+ background: linear-gradient(145deg, rgba(177,87,24,.52), rgba(95,45,17,.48));
+ box-shadow: 0 0 13px rgba(242,129,38,.15), inset 0 1px 0 rgba(255,228,198,.18);
+}
+
+.schedule-dialog {
+ width: min(72rem, calc(100vw - 2rem));
+ max-width: none;
+ max-height: calc(100vh - 2rem);
+ padding: 0;
+ border: 1px solid var(--panel-edge);
+ border-radius: 1.35rem;
+ color: var(--panel-ink);
+ background:
+ linear-gradient(125deg, rgba(255,255,255,.28), transparent 32%, rgba(255,255,255,.06) 72%),
+ rgba(201,229,244,.72);
+ box-shadow: 0 28px 90px rgba(8,45,70,.42), inset 0 1px 0 rgba(255,255,255,.9);
+ backdrop-filter: blur(28px) saturate(135%);
+ -webkit-backdrop-filter: blur(28px) saturate(135%);
+ overflow: hidden;
+}
+.schedule-dialog::backdrop { background: rgba(5,20,31,.46); backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px); }
+:root[data-theme="dark"] .schedule-dialog { background: linear-gradient(125deg, rgba(205,239,255,.1), transparent 34%), rgba(7,24,36,.88); box-shadow: 0 30px 100px rgba(0,0,0,.64), inset 0 1px 0 rgba(221,243,252,.15); }
+.schedule-editor { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; max-height: calc(100vh - 2rem); }
+.editor-header, .editor-footer { display: flex; align-items: center; gap: .7rem; padding: .8rem 1rem; }
+.editor-header { justify-content: space-between; border-bottom: 1px solid var(--tile-line-soft); }
+.editor-heading { display: flex; align-items: center; gap: .65rem; }
+.editor-heading > svg { width: 2rem; height: 2rem; color: #1883df; filter: drop-shadow(0 0 5px rgba(24,131,223,.2)); }
+.editor-heading h2 { margin: 0; font-size: 1.15rem; }
+.editor-heading small { display: block; margin-top: .15rem; color: var(--panel-muted); font-size: .63rem; }
+.editor-close { width: 2.2rem; height: 2.2rem; padding: 0; border: 1px solid var(--tile-line); border-radius: .75rem; color: var(--panel-muted); background: var(--tile); cursor: pointer; font-size: 1.45rem; line-height: 1; }
+.editor-scroll { display: grid; gap: .7rem; padding: .85rem 1rem 1rem; overflow: auto; }
+.schedule-enabled { display: flex; align-items: center; gap: .7rem; padding: .72rem .82rem; border: 1px solid var(--tile-line); border-radius: .9rem; background: linear-gradient(145deg, rgba(255,255,255,.22), rgba(120,169,195,.08)); cursor: pointer; }
+.schedule-enabled input { width: 1.1rem; height: 1.1rem; accent-color: var(--blue); }
+.schedule-enabled span { display: grid; gap: .12rem; }
+.schedule-enabled strong { font-size: .76rem; }
+.schedule-enabled small { color: var(--panel-muted); font-size: .61rem; }
+.editor-section { padding: .8rem; border: 1px solid var(--tile-line); border-radius: 1rem; background: linear-gradient(145deg, rgba(255,255,255,.19), rgba(94,148,179,.075)); box-shadow: inset 0 1px 0 rgba(255,255,255,.48); }
+:root[data-theme="dark"] .editor-section, :root[data-theme="dark"] .schedule-enabled { background: linear-gradient(145deg, rgba(205,236,249,.055), rgba(2,19,30,.12)); box-shadow: inset 0 1px 0 rgba(224,246,255,.07); }
+.editor-section-title { display: flex; align-items: end; justify-content: space-between; gap: .75rem; margin-bottom: .65rem; }
+.editor-section-title h3 { margin: 0; font-size: .86rem; }
+.editor-section-title small { display: block; margin-top: .13rem; color: var(--panel-muted); font-size: .6rem; }
+.day-assignments { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: .42rem; }
+.day-assignment { display: grid; gap: .3rem; color: var(--panel-muted); font-size: .58rem; font-weight: 700; }
+.day-assignment select, .template-toolbar select, .point-editor input, .point-editor select {
+ min-width: 0;
+ height: 2.25rem;
+ padding: .38rem .48rem;
+ border: 1px solid var(--tile-line);
+ border-radius: .62rem;
+ color: var(--panel-ink);
+ background: rgba(245,251,253,.48);
+ outline: none;
+ font-size: .66rem;
+}
+:root[data-theme="dark"] .day-assignment select,
+:root[data-theme="dark"] .template-toolbar select,
+:root[data-theme="dark"] .point-editor input,
+:root[data-theme="dark"] .point-editor select { background: rgba(16,38,52,.72); }
+.day-assignment select:focus, .template-toolbar select:focus, .point-editor input:focus, .point-editor select:focus { border-color: rgba(45,151,230,.7); box-shadow: 0 0 0 3px rgba(34,145,226,.11); }
+.template-toolbar > label { display: grid; gap: .25rem; min-width: 10rem; color: var(--panel-muted); font-size: .57rem; font-weight: 700; }
+.template-actions { display: flex; align-items: end; gap: .35rem; }
+.template-actions button { min-height: 2.25rem; padding: .4rem .65rem; border: 1px solid var(--tile-line); border-radius: .62rem; color: #126fc4; background: rgba(52,150,220,.08); cursor: pointer; font-size: .61rem; font-weight: 700; }
+.template-actions #delete-template { color: var(--red); border-color: rgba(223,91,105,.28); background: var(--red-soft); }
+.template-actions button:disabled { cursor: not-allowed; opacity: .45; }
+.schedule-points { display: grid; gap: .55rem; }
+.point-editor { padding: .68rem; border: 1px solid var(--tile-line); border-radius: .85rem; background: linear-gradient(145deg, rgba(255,255,255,.22), rgba(116,160,183,.065)); box-shadow: inset 0 1px 0 rgba(255,255,255,.45); }
+:root[data-theme="dark"] .point-editor { background: linear-gradient(145deg, rgba(197,231,247,.055), rgba(2,18,29,.13)); box-shadow: inset 0 1px 0 rgba(224,246,255,.06); }
+.point-header { display: grid; grid-template-columns: auto minmax(7rem, .7fr) minmax(9rem, 1fr) auto; align-items: end; gap: .55rem; }
+.point-number { align-self: center; display: grid; place-items: center; width: 1.85rem; height: 1.85rem; border-radius: 50%; color: #1478cc; background: var(--blue-soft); font-size: .62rem; font-weight: 800; }
+.point-header label, .point-setting { display: grid; gap: .25rem; color: var(--panel-muted); font-size: .56rem; font-weight: 700; }
+.remove-point { width: 2.25rem; height: 2.25rem; padding: 0; border: 1px solid rgba(223,91,105,.32); border-radius: .62rem; color: var(--red); background: var(--red-soft); cursor: pointer; font-size: 1.1rem; }
+.point-settings { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: .48rem; margin-top: .55rem; }
+.point-editor.auto [data-set-only] { display: none; }
+.point-editor.auto .point-settings { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.empty-points { padding: 1.4rem; border: 1px dashed var(--tile-line); border-radius: .8rem; color: var(--panel-muted); text-align: center; font-size: .68rem; }
+.add-schedule-point { width: 100%; min-height: 2.55rem; margin-top: .6rem; border: 1px dashed rgba(37,131,205,.55); border-radius: .75rem; color: #126fc4; background: rgba(52,150,220,.08); cursor: pointer; font-size: .68rem; font-weight: 750; }
+.editor-footer { border-top: 1px solid var(--tile-line-soft); justify-content: flex-end; }
+.editor-status { margin-right: auto; color: var(--panel-muted); font-size: .63rem; }
+.editor-status.error { color: var(--red); }
+.editor-button { min-height: 2.5rem; padding: .58rem .9rem; border-radius: .75rem; cursor: pointer; font-size: .69rem; font-weight: 750; }
+.editor-button.secondary { border: 1px solid var(--tile-line); color: var(--panel-ink); background: var(--tile); }
+.editor-button.primary { border: 1px solid rgba(182,226,255,.76); color: white; background: radial-gradient(circle at 20% -30%, rgba(255,255,255,.5), transparent 48%), linear-gradient(145deg, #3aa9eb, #1476c9); box-shadow: 0 7px 17px rgba(18,102,170,.2), inset 0 1px 0 rgba(255,255,255,.75); }
+
+@media (max-width: 900px) {
+ .day-assignments { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+ .point-settings { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+
+@media (max-width: 600px) {
+ .schedule-dialog { width: calc(100vw - .7rem); max-height: calc(100vh - .7rem); border-radius: 1rem; }
+ .schedule-editor { max-height: calc(100vh - .7rem); }
+ .editor-header, .editor-footer { padding: .7rem; }
+ .editor-scroll { padding: .7rem; }
+ .day-assignments { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .editor-section-title { align-items: stretch; flex-direction: column; }
+ .template-toolbar > label { min-width: 0; }
+ .template-actions { align-items: stretch; }
+ .template-actions button { flex: 1; }
+ .point-header { grid-template-columns: auto 1fr auto; }
+ .point-header .point-action { grid-column: 2 / -1; }
+ .point-settings, .point-editor.auto .point-settings { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .editor-footer { flex-wrap: wrap; }
+ .editor-status { width: 100%; }
+ .schedule-edit-button span { display: none; }
+}
diff --git a/web/css/widget.css b/web/css/widget.css
new file mode 100644
index 0000000..b0e0ce3
--- /dev/null
+++ b/web/css/widget.css
@@ -0,0 +1,466 @@
+:root {
+ --widget-ink: #08265d;
+ --widget-muted: #315d91;
+ --widget-glass: rgba(200, 228, 244, 0.21);
+ --widget-edge: rgba(255, 255, 255, 0.9);
+ --tile: rgba(104, 126, 139, 0.16);
+ --tile-strong: rgba(214, 226, 232, 0.2);
+ --tile-edge: rgba(255, 255, 255, 0.82);
+ --tile-edge-soft: rgba(189, 211, 221, 0.38);
+ --tile-shadow: 0 10px 25px rgba(24, 72, 105, 0.12), inset 0 1px 0 rgba(255,255,255,.78), inset 1px 0 0 rgba(255,255,255,.24);
+ --blue-halo: rgba(23, 137, 248, 0.16);
+ --green-halo: rgba(28, 196, 113, 0.16);
+ --orange-halo: rgba(246, 126, 50, 0.14);
+}
+
+:root[data-theme="dark"] {
+ --widget-ink: #edf8ff;
+ --widget-muted: #9fc5df;
+ --widget-glass: rgba(7, 22, 33, 0.49);
+ --widget-edge: rgba(187, 225, 244, 0.3);
+ --tile: rgba(16, 37, 51, 0.3);
+ --tile-strong: rgba(52, 78, 96, 0.33);
+ --tile-edge: rgba(203, 234, 248, 0.25);
+ --tile-edge-soft: rgba(114, 176, 207, 0.16);
+ --tile-shadow: 0 13px 29px rgba(0,0,0,.26), inset 0 1px 0 rgba(226,246,255,.11), inset 1px 0 0 rgba(186,226,245,.055);
+ --blue-halo: rgba(29, 153, 245, 0.22);
+ --green-halo: rgba(27, 210, 121, 0.19);
+ --orange-halo: rgba(246, 125, 46, 0.18);
+}
+
+html { min-width: 280px; }
+
+body {
+ min-height: 100vh;
+ margin: 0;
+ padding: clamp(.5rem, 2.2vw, 1rem);
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ color: var(--widget-ink);
+ background-color: #b7d5e3;
+ background-image:
+ linear-gradient(118deg, rgba(180,220,239,.15), rgba(238,247,251,.025) 45%, rgba(247,226,204,.09)),
+ url("../assets/mountain-lake-light.jpg");
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-attachment: fixed;
+ overflow-x: hidden;
+ isolation: isolate;
+}
+
+:root[data-theme="dark"] body {
+ background-color: #091722;
+ background-image:
+ linear-gradient(118deg, rgba(2,15,25,.66), rgba(8,27,40,.57) 48%, rgba(20,25,29,.64)),
+ url("../assets/mountain-lake-dark.jpg");
+}
+
+body::before,
+body::after {
+ content: "";
+ position: fixed;
+ inset: 0;
+ z-index: -2;
+ pointer-events: none;
+}
+
+body::before {
+ background: linear-gradient(110deg, rgba(214,242,253,.16), transparent 35%, rgba(255,240,218,.1) 76%, transparent);
+ opacity: .74;
+}
+
+body::after {
+ background:
+ radial-gradient(circle at 11% 18%, rgba(255,255,255,.32) 0 .5%, transparent 5%),
+ radial-gradient(circle at 64% 23%, rgba(255,244,218,.27) 0 .6%, transparent 6%),
+ radial-gradient(circle at 91% 79%, rgba(225,250,237,.21) 0 .8%, transparent 7%);
+ filter: blur(11px);
+ opacity: .5;
+}
+
+:root[data-theme="dark"] body::before { background: linear-gradient(115deg, rgba(29,104,142,.1), transparent 40%, rgba(110,76,42,.08)); opacity: .7; }
+:root[data-theme="dark"] body::after { opacity: .25; }
+
+.ambient::before { width: 32rem; height: 32rem; left: -13rem; top: -14rem; background: #c9f2ff; opacity: .14; filter: blur(76px); }
+.ambient::after { width: 34rem; height: 34rem; right: -14rem; bottom: -16rem; background: #ebd5a7; opacity: .11; filter: blur(82px); }
+:root[data-theme="dark"] .ambient::before { background: #147db5; opacity: .12; }
+:root[data-theme="dark"] .ambient::after { background: #846630; opacity: .08; }
+
+.widget {
+ position: relative;
+ isolation: isolate;
+ width: min(100%, 31.5rem);
+ display: grid;
+ gap: .65rem;
+ padding: clamp(.65rem, 2.2vw, .9rem);
+ overflow: hidden;
+ border: 1px solid var(--widget-edge);
+ outline: 1px solid rgba(255,255,255,.48);
+ outline-offset: -5px;
+ border-radius: 1.55rem;
+ background:
+ linear-gradient(125deg, rgba(255,255,255,.22), transparent 31%, rgba(255,255,255,.055) 69%, rgba(196,224,240,.1)),
+ var(--widget-glass);
+ box-shadow: 0 27px 68px rgba(22,66,95,.24), 0 2px 8px rgba(255,255,255,.28), inset 0 1px 0 rgba(255,255,255,.88), inset 0 -1px 0 rgba(113,163,192,.17);
+ backdrop-filter: blur(13px) saturate(124%);
+ -webkit-backdrop-filter: blur(13px) saturate(124%);
+}
+
+.widget::before,
+.widget::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ border-radius: inherit;
+ pointer-events: none;
+}
+
+.widget::before {
+ background:
+ linear-gradient(112deg, rgba(255,255,255,.25), rgba(255,255,255,.035) 18%, transparent 39%),
+ radial-gradient(ellipse at 50% -20%, rgba(255,255,255,.25), transparent 47%);
+}
+
+.widget::after {
+ opacity: .1;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
+ mix-blend-mode: soft-light;
+}
+
+.widget > * { position: relative; z-index: 1; }
+:root[data-theme="dark"] .widget { outline-color: rgba(203,235,250,.11); box-shadow: 0 33px 78px rgba(0,0,0,.43), 0 0 28px rgba(19,112,158,.06), inset 0 1px 0 rgba(221,243,252,.17), inset 0 -1px 0 rgba(0,0,0,.28); }
+:root[data-theme="dark"] .widget::before { background: linear-gradient(112deg, rgba(203,238,253,.1), transparent 24%, transparent 72%, rgba(71,163,202,.035)); }
+
+.widget-header {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: .55rem .7rem;
+ padding: .15rem .25rem .05rem;
+}
+
+.identity { min-width: 0; display: flex; align-items: center; gap: .7rem; }
+.brand-icon {
+ display: grid;
+ place-items: center;
+ width: 2.9rem;
+ height: 2.9rem;
+ flex: 0 0 auto;
+ border: 1px solid rgba(255,255,255,.82);
+ border-radius: 1rem;
+ color: #147bd5;
+ background:
+ radial-gradient(circle at 25% 2%, rgba(255,255,255,.62), transparent 43%),
+ linear-gradient(145deg, rgba(217,242,255,.58), rgba(73,159,222,.2));
+ box-shadow: 0 8px 19px rgba(19,101,160,.13), inset 0 1px 0 rgba(255,255,255,.88);
+}
+.brand-icon svg { width: 1.75rem; height: 1.75rem; filter: drop-shadow(0 0 5px rgba(20,123,213,.18)); }
+.identity-copy { min-width: 0; display: block; }
+.title-row { display: flex; align-items: center; gap: .55rem; min-width: 0; }
+.identity h1 { margin: 0; font-size: 1.62rem; line-height: 1; letter-spacing: -.045em; white-space: nowrap; }
+.identity-copy > small { display: block; margin-top: .28rem; color: var(--widget-muted); font-size: .7rem; }
+.widget .pill { min-height: 1.75rem; padding: .27rem .58rem; border-color: rgba(255,255,255,.56); font-size: .59rem; box-shadow: inset 0 1px 0 rgba(255,255,255,.62), 0 5px 14px rgba(26,75,108,.08); backdrop-filter: blur(12px); }
+.widget .pill::before { width: .36rem; height: .36rem; box-shadow: 0 0 7px currentColor; }
+
+.theme-button,
+.open-section {
+ display: grid;
+ place-items: center;
+ border: 1px solid var(--tile-edge);
+ color: var(--widget-ink);
+ background: linear-gradient(145deg, var(--tile-strong), var(--tile));
+ box-shadow: var(--tile-shadow);
+ cursor: pointer;
+ backdrop-filter: blur(14px);
+ -webkit-backdrop-filter: blur(14px);
+ transition: transform .15s ease, filter .15s ease;
+}
+.theme-button { width: 2.55rem; height: 2.55rem; border-radius: .9rem; }
+.theme-button svg { width: 1.18rem; height: 1.18rem; }
+.theme-button:hover, .open-section:hover { transform: translateY(-1px); filter: brightness(1.07); }
+.theme-button:active, .open-section:active { transform: scale(.97); }
+.status-row { grid-column: 1 / -1; display: flex; align-items: center; gap: .55rem; min-width: 0; }
+.mode-detail { min-width: 0; color: var(--widget-muted); font-size: .66rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.widget-tile {
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+ border: 1px solid var(--tile-edge);
+ outline: 1px solid var(--tile-edge-soft);
+ outline-offset: -3px;
+ border-radius: 1.15rem;
+ background:
+ linear-gradient(132deg, rgba(244,248,250,.22), rgba(159,178,188,.055) 38%, transparent 66%),
+ linear-gradient(145deg, var(--tile-strong), var(--tile));
+ box-shadow: var(--tile-shadow);
+ backdrop-filter: blur(14px) saturate(116%);
+ -webkit-backdrop-filter: blur(14px) saturate(116%);
+}
+
+.widget-tile::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ border-radius: inherit;
+ pointer-events: none;
+ opacity: .62;
+ background: linear-gradient(115deg, rgba(255,255,255,.35), transparent 25%, transparent 72%, rgba(255,255,255,.08));
+}
+.widget-tile > * { position: relative; z-index: 1; }
+:root[data-theme="dark"] .widget-tile { background: linear-gradient(132deg, rgba(213,240,252,.075), rgba(255,255,255,.012) 38%, transparent 65%), linear-gradient(145deg, var(--tile-strong), var(--tile)); backdrop-filter: blur(16px) saturate(127%); -webkit-backdrop-filter: blur(16px) saturate(127%); }
+
+.control-card { min-height: 11.4rem; padding: .9rem 1rem .85rem; display: flex; flex-direction: column; }
+.control-card::after,
+.switch-card::after {
+ content: "";
+ position: absolute;
+ z-index: 0;
+ width: 12rem;
+ height: 12rem;
+ right: -5rem;
+ bottom: -7rem;
+ border-radius: 50%;
+ pointer-events: none;
+ filter: blur(42px);
+ opacity: .18;
+}
+.speed-card::after { background: var(--blue-halo); }
+.temperature-card::after { width: 17rem; background: linear-gradient(90deg, var(--blue-halo), var(--orange-halo)); }
+.power-card::after, .heater-card::after { background: var(--green-halo); opacity: .16; }
+
+.card-title { display: flex; align-items: flex-start; gap: .65rem; }
+.card-title h2, .switch-title h2, .section-title h2 { margin: 0; font-size: .96rem; line-height: 1.2; letter-spacing: -.015em; }
+.card-title p { margin: .16rem 0 0; color: var(--widget-muted); font-size: .66rem; }
+.ui-icon { display: block; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
+.feature-icon { width: 1.95rem; height: 1.95rem; flex: 0 0 auto; padding: .06rem; color: #1389f4; filter: drop-shadow(0 0 5px rgba(20,139,244,.2)); }
+
+.hero-control { display: grid; grid-template-columns: 3.45rem 1fr 3.45rem; align-items: center; gap: .8rem; margin: 1rem 0 .72rem; }
+.hero-value { text-align: center; font-size: 3rem; line-height: 1; letter-spacing: -.065em; font-variant-numeric: tabular-nums; }
+.hero-step {
+ display: grid;
+ place-items: center;
+ width: 3.45rem;
+ height: 3.45rem;
+ padding: 0;
+ border: 1.5px solid rgba(255,255,255,.84);
+ border-radius: .95rem;
+ color: #176dbb;
+ background:
+ radial-gradient(circle at 24% 5%, rgba(255,255,255,.48), transparent 40%),
+ linear-gradient(145deg, rgba(245,252,255,.4), rgba(191,224,244,.18));
+ box-shadow: 0 8px 19px rgba(35,92,132,.13), inset 0 1px 0 rgba(255,255,255,.88), 0 0 0 1px rgba(185,226,248,.2);
+ cursor: pointer;
+ font-size: 1.8rem;
+ line-height: 1;
+ transition: transform .15s ease, filter .15s ease, box-shadow .15s ease;
+}
+.hero-step.primary {
+ color: white;
+ background:
+ radial-gradient(circle at 24% 5%, rgba(255,255,255,.55), transparent 38%),
+ linear-gradient(135deg, rgba(255,255,255,.16), transparent 42%, rgba(0,73,170,.1)),
+ linear-gradient(145deg, #55b4ff, #1479e8 72%);
+ box-shadow: 0 8px 18px rgba(15,92,183,.18), 0 0 7px rgba(48,151,239,.1), inset 0 1px 0 rgba(255,255,255,.86), inset 0 -3px 7px rgba(0,67,156,.16);
+}
+.hero-step:hover { transform: translateY(-1px); filter: brightness(1.07); box-shadow: 0 10px 21px rgba(20,101,190,.2), 0 0 9px rgba(48,151,239,.12), inset 0 1px 0 rgba(255,255,255,.88); }
+.hero-step:active { transform: scale(.97); }
+
+.speed-segments { display: grid; grid-template-columns: repeat(6, 1fr); gap: .18rem; }
+.speed-segments button {
+ height: .9rem;
+ padding: 0;
+ border: 1px solid rgba(255,255,255,.82);
+ border-radius: .5rem;
+ background: linear-gradient(180deg, rgba(173,207,229,.3), rgba(98,151,190,.18));
+ box-shadow: inset 0 1px 2px rgba(27,70,105,.07), 0 2px 5px rgba(32,86,126,.07);
+ cursor: pointer;
+ transition: background .12s ease, box-shadow .12s ease, transform .12s ease;
+}
+.speed-segments button:hover { transform: translateY(-1px); }
+.speed-segments button.active {
+ background: linear-gradient(180deg, rgba(255,255,255,.32), transparent 48%), linear-gradient(90deg, #1685ec, #35afff);
+ box-shadow: 0 0 5px rgba(25,143,247,.19), inset 0 1px 0 rgba(255,255,255,.75), inset 0 -2px 4px rgba(0,84,182,.13);
+}
+.scale-labels, .temperature-labels { display: flex; justify-content: space-between; margin-top: .34rem; padding: 0 .3rem; color: var(--widget-muted); font-size: .62rem; }
+
+.temperature-range { width: 100%; height: 1.45rem; margin: 0; appearance: none; background: transparent; cursor: pointer; }
+.temperature-range::-webkit-slider-runnable-track { height: .68rem; border: 1px solid rgba(255,255,255,.88); border-radius: 999px; background: linear-gradient(90deg, #0878ed, #53c7f5 38%, #f3c26e 72%, #ff6b31); box-shadow: 0 0 8px rgba(37,153,239,.15), 0 0 7px rgba(255,114,41,.09), inset 0 1px 2px rgba(255,255,255,.46); }
+.temperature-range::-moz-range-track { height: .68rem; border: 1px solid rgba(255,255,255,.88); border-radius: 999px; background: linear-gradient(90deg, #0878ed, #53c7f5 38%, #f3c26e 72%, #ff6b31); }
+.temperature-range::-moz-range-progress { background: transparent; }
+.temperature-range::-webkit-slider-thumb { width: 1.6rem; height: 1.6rem; margin-top: -.49rem; appearance: none; border: .25rem solid rgba(244,252,255,.9); border-radius: 50%; background: #238feb; box-shadow: 0 0 0 2px rgba(255,255,255,.45), 0 0 11px rgba(29,139,241,.31), 0 4px 9px rgba(17,79,143,.22); }
+.temperature-range::-moz-range-thumb { width: 1.08rem; height: 1.08rem; border: .25rem solid rgba(244,252,255,.9); border-radius: 50%; background: #238feb; box-shadow: 0 0 0 2px rgba(255,255,255,.45), 0 0 11px rgba(29,139,241,.31), 0 4px 9px rgba(17,79,143,.22); }
+
+.switch-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .65rem; }
+.switch-card { min-width: 0; min-height: 8.15rem; padding: .82rem; display: flex; flex-direction: column; gap: .72rem; }
+.switch-title { display: flex; align-items: center; gap: .5rem; min-width: 0; }
+.switch-title > span { min-width: 0; }
+.switch-title small { display: block; margin-top: .12rem; color: var(--widget-muted); font-size: .58rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.switch-icon { width: 1.75rem; height: 1.75rem; flex: 0 0 auto; }
+.switch-icon.green { color: #08a95a; filter: drop-shadow(0 0 5px rgba(20,190,104,.23)); }
+.switch-icon.orange { color: #f06419; filter: drop-shadow(0 0 5px rgba(244,105,31,.2)); }
+.glass-switch {
+ position: relative;
+ overflow: hidden;
+ width: 100%;
+ min-height: 3.35rem;
+ margin-top: auto;
+ border: 1.5px solid rgba(255,255,255,.86);
+ border-radius: .95rem;
+ color: var(--widget-ink);
+ background: rgba(199,222,235,.24);
+ box-shadow: var(--tile-shadow);
+ cursor: pointer;
+ font-size: 1.25rem;
+ font-weight: 850;
+ text-shadow: 0 1px 0 rgba(255,255,255,.3);
+ transition: transform .15s ease, background .2s ease, box-shadow .2s ease, filter .2s ease;
+}
+.glass-switch:hover { transform: translateY(-1px); filter: brightness(1.06); }
+.glass-switch:active { transform: scale(.98); }
+.glass-switch[aria-pressed="true"].green,
+.glass-switch[aria-pressed="true"].orange {
+ background:
+ radial-gradient(circle at 25% -8%, rgba(255,255,255,.67), transparent 42%),
+ linear-gradient(132deg, rgba(255,255,255,.15), transparent 40%, rgba(0,125,62,.06)),
+ linear-gradient(145deg, rgba(139,244,190,.67), rgba(55,194,122,.43));
+ border-color: rgba(218,255,235,.84);
+ box-shadow: 0 8px 19px rgba(18,143,80,.14), 0 0 8px rgba(38,199,116,.09), inset 0 1px 0 rgba(255,255,255,.9), inset 0 -4px 9px rgba(0,114,57,.08);
+}
+
+.info-card { padding: .82rem; }
+.section-title { display: flex; align-items: center; justify-content: space-between; gap: .65rem; }
+.section-title > span { display: flex; align-items: center; gap: .5rem; }
+.section-icon { width: 1.75rem; height: 1.75rem; color: #207bcc; filter: drop-shadow(0 0 5px rgba(26,124,210,.17)); }
+.section-icon.leaf { color: #069c51; transform: rotate(-18deg); filter: drop-shadow(0 0 5px rgba(8,160,81,.2)); }
+.open-section { width: 2rem; height: 2rem; padding: 0; border-radius: .7rem; color: #147bd5; font-size: .96rem; }
+
+.co2-summary { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .6rem; margin: .8rem .1rem .52rem; }
+.co2-label { color: var(--widget-muted); font-size: .74rem; font-weight: 750; }
+.co2-summary strong { text-align: center; font-size: 2rem; line-height: 1; letter-spacing: -.045em; }
+.co2-summary strong small { color: var(--widget-muted); font-size: .62rem; font-weight: 650; letter-spacing: 0; }
+.quality-chip { display: inline-flex; justify-content: center; padding: .32rem .62rem; border: 1px solid var(--tile-edge-soft); border-radius: 999px; background: rgba(199,222,235,.2); color: var(--widget-muted); font-size: .6rem; font-weight: 750; white-space: nowrap; }
+.quality-chip.good { color: #076d3d; border-color: rgba(109,237,167,.65); background: linear-gradient(145deg, rgba(179,255,214,.58), rgba(92,218,151,.39)); box-shadow: 0 0 9px rgba(33,203,116,.12), inset 0 1px 0 rgba(255,255,255,.82); }
+.quality-chip.warn { color: #9c5513; border-color: rgba(244,183,106,.67); background: linear-gradient(145deg, rgba(255,232,188,.56), rgba(239,168,80,.34)); }
+.quality-chip.bad { color: #a62f3d; border-color: rgba(242,135,147,.62); background: linear-gradient(145deg, rgba(255,209,216,.53), rgba(225,105,120,.3)); }
+:root[data-theme="dark"] .quality-chip.good { color: #b9ffda; }
+:root[data-theme="dark"] .quality-chip.warn { color: #ffd59e; }
+:root[data-theme="dark"] .quality-chip.bad { color: #ffc0c8; }
+
+.co2-meter { height: .68rem; border: 1px solid rgba(255,255,255,.78); border-radius: 999px; background: rgba(91,145,181,.17); box-shadow: inset 0 1px 3px rgba(25,68,98,.09); overflow: hidden; }
+.co2-meter i { display: block; width: 0; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #08a350, #36dc83); box-shadow: 0 0 10px rgba(18,199,104,.34), inset 0 1px 0 rgba(255,255,255,.55); transition: width .3s ease; }
+.meter-labels { display: flex; justify-content: space-between; margin-top: .26rem; padding: 0 .2rem; color: var(--widget-muted); font-size: .54rem; }
+.mini-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: .52rem; margin-top: .68rem; }
+.mini-metric { min-width: 0; min-height: 4.35rem; display: flex; align-items: center; justify-content: center; gap: .5rem; padding: .62rem .5rem; border: 1px solid var(--tile-edge-soft); border-radius: .9rem; background: linear-gradient(145deg, rgba(255,255,255,.13), rgba(107,151,177,.055)); box-shadow: inset 0 1px 0 rgba(255,255,255,.33); text-align: center; }
+.metric-icon { width: 1.55rem; height: 1.55rem; flex: 0 0 auto; color: #137edc; filter: drop-shadow(0 0 4px rgba(20,124,221,.15)); }
+.mini-metric span { min-width: 0; }
+.mini-metric small { display: block; color: var(--widget-muted); font-size: .58rem; }
+.mini-metric strong { display: block; margin-top: .2rem; font-size: 1.35rem; line-height: 1; letter-spacing: -.035em; }
+
+.schedule-times { display: grid; grid-template-columns: 1fr 1px 1fr; align-items: center; gap: .7rem; margin-top: .75rem; padding: .15rem .25rem .25rem; }
+.schedule-times > span { display: grid; justify-items: center; gap: .18rem; text-align: center; }
+.schedule-times small { color: var(--widget-muted); font-size: .6rem; }
+.schedule-times strong { font-size: 1.35rem; letter-spacing: -.035em; }
+.schedule-times > i { width: 1px; height: 2.75rem; background: var(--tile-edge-soft); }
+
+.open-panel {
+ min-height: 3.25rem;
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ gap: .65rem;
+ padding: .65rem .9rem;
+ border: 1px solid rgba(194,232,255,.84);
+ border-radius: 1rem;
+ color: white;
+ background:
+ radial-gradient(circle at 18% -35%, rgba(255,255,255,.52), transparent 47%),
+ linear-gradient(135deg, rgba(255,255,255,.13), transparent 43%),
+ linear-gradient(145deg, rgba(60,174,239,.92), rgba(17,113,194,.88));
+ box-shadow: 0 9px 20px rgba(15,92,183,.2), 0 0 9px rgba(48,151,239,.11), inset 0 1px 0 rgba(255,255,255,.84), inset 0 -3px 7px rgba(0,67,156,.15);
+ cursor: pointer;
+ font-size: .78rem;
+ font-weight: 760;
+ transition: transform .15s ease, filter .15s ease, box-shadow .15s ease;
+}
+.open-panel svg { width: 1.45rem; height: 1.45rem; }
+.open-panel b { font-size: 1rem; }
+.open-panel:hover { transform: translateY(-1px); filter: brightness(1.05); box-shadow: 0 11px 23px rgba(20,101,190,.23), 0 0 11px rgba(48,151,239,.13), inset 0 1px 0 rgba(255,255,255,.86); }
+.open-panel:active { transform: scale(.99); }
+.demo-note { color: var(--widget-muted); text-align: center; font-size: .58rem; }
+
+:root[data-theme="dark"] .brand-icon { border-color: rgba(193,228,245,.25); background: radial-gradient(circle at 25% 2%, rgba(208,243,255,.16), transparent 43%), linear-gradient(145deg, rgba(38,125,187,.27), rgba(15,61,91,.26)); }
+:root[data-theme="dark"] .hero-step { color: #76c7fa; border-color: rgba(192,226,244,.25); background: radial-gradient(circle at 24% 5%, rgba(213,240,252,.11), transparent 40%), linear-gradient(145deg, rgba(77,112,133,.23), rgba(20,44,59,.28)); box-shadow: 0 9px 21px rgba(0,0,0,.23), inset 0 1px 0 rgba(226,246,255,.12); }
+:root[data-theme="dark"] .hero-step.primary { color: white; background: radial-gradient(circle at 24% 5%, rgba(214,246,255,.32), transparent 38%), linear-gradient(135deg, rgba(255,255,255,.08), transparent 42%, rgba(0,32,95,.17)), linear-gradient(145deg, #33aaf4, #0869d0 74%); box-shadow: 0 10px 24px rgba(0,53,119,.38), 0 0 11px rgba(41,162,244,.19), inset 0 1px 0 rgba(220,248,255,.4), inset 0 -3px 7px rgba(0,27,91,.27); }
+:root[data-theme="dark"] .speed-segments button { border-color: rgba(192,226,244,.25); background: linear-gradient(180deg, rgba(91,136,162,.23), rgba(24,55,74,.28)); }
+:root[data-theme="dark"] .speed-segments button.active { border-color: rgba(117,207,255,.48); background: linear-gradient(180deg, rgba(216,246,255,.19), transparent 48%), linear-gradient(90deg, #0877df, #20b7ff); box-shadow: 0 0 7px rgba(24,156,255,.27), inset 0 1px 0 rgba(223,248,255,.34); }
+:root[data-theme="dark"] .glass-switch { color: #ddecf4; border-color: rgba(193,228,245,.25); background: linear-gradient(145deg, rgba(77,112,133,.23), rgba(20,44,59,.28)); text-shadow: 0 1px 2px rgba(0,0,0,.4); }
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].green,
+:root[data-theme="dark"] .glass-switch[aria-pressed="true"].orange { color: #effff6; border-color: rgba(127,242,182,.4); background: radial-gradient(circle at 25% -8%, rgba(218,255,236,.3), transparent 42%), linear-gradient(132deg, rgba(255,255,255,.07), transparent 40%, rgba(0,45,23,.12)), linear-gradient(145deg, rgba(51,190,119,.52), rgba(9,102,59,.49)); box-shadow: 0 9px 21px rgba(0,0,0,.22), 0 0 9px rgba(37,207,121,.1), inset 0 1px 0 rgba(218,255,236,.32), inset 0 -3px 8px rgba(0,31,16,.16); }
+:root[data-theme="dark"] .co2-meter { border-color: rgba(200,232,247,.24); background: rgba(3,22,34,.34); box-shadow: inset 0 2px 5px rgba(0,0,0,.2); }
+:root[data-theme="dark"] .mini-metric { background: linear-gradient(145deg, rgba(196,231,247,.055), rgba(3,24,37,.08)); box-shadow: inset 0 1px 0 rgba(224,246,255,.07); }
+
+/* Wide monitor layout. The same markup stays vertical on narrow screens, so
+ controls and API behaviour cannot drift between two widget versions. */
+:root[data-widget-layout="horizontal"] .widget {
+ width: min(100%, 76rem);
+ max-width: none;
+ grid-template-columns: minmax(0, 1.4fr) minmax(0, 1.4fr) minmax(0, .8fr) minmax(0, .8fr);
+ grid-template-areas:
+ "header header header header"
+ "speed temperature power heater"
+ "air air schedule schedule"
+ "open open open open"
+ "demo demo demo demo";
+ align-items: stretch;
+}
+:root[data-widget-layout="horizontal"] .widget-header { grid-area: header; grid-template-columns: minmax(0, 1fr) auto auto; }
+:root[data-widget-layout="horizontal"] .widget-header .identity { grid-column: 1; grid-row: 1; }
+:root[data-widget-layout="horizontal"] .widget-header .status-row { grid-column: 2; grid-row: 1; }
+:root[data-widget-layout="horizontal"] .widget-header .theme-button { grid-column: 3; grid-row: 1; }
+:root[data-widget-layout="horizontal"] .speed-card { grid-area: speed; }
+:root[data-widget-layout="horizontal"] .temperature-card { grid-area: temperature; }
+:root[data-widget-layout="horizontal"] .switch-grid { display: contents; }
+:root[data-widget-layout="horizontal"] .switch-grid > * { z-index: 1; }
+:root[data-widget-layout="horizontal"] .power-card { grid-area: power; }
+:root[data-widget-layout="horizontal"] .heater-card { grid-area: heater; }
+:root[data-widget-layout="horizontal"] .switch-card { min-height: 11.4rem; }
+:root[data-widget-layout="horizontal"] .air-card { grid-area: air; }
+:root[data-widget-layout="horizontal"] .schedule-card { grid-area: schedule; }
+:root[data-widget-layout="horizontal"] .air-card,
+:root[data-widget-layout="horizontal"] .schedule-card { min-height: 9.6rem; }
+:root[data-widget-layout="horizontal"] .open-panel { grid-area: open; }
+:root[data-widget-layout="horizontal"] .demo-note { grid-area: demo; }
+
+button:disabled { opacity: .58; cursor: wait; transform: none !important; }
+
+@media (min-width: 560px) and (min-height: 900px) {
+ html { font-size: 17px; }
+ :root[data-widget-layout="vertical"] .widget { width: min(100%, 33rem); }
+}
+
+@media (max-width: 380px) {
+ body { padding: .36rem; }
+ .widget { padding: .55rem; gap: .52rem; border-radius: 1.2rem; }
+ .brand-icon { width: 2.55rem; height: 2.55rem; }
+ .identity h1 { font-size: 1.38rem; }
+ .title-row { gap: .38rem; }
+ .widget .pill { padding-inline: .46rem; font-size: .54rem; }
+ .hero-control { grid-template-columns: 3.15rem 1fr 3.15rem; gap: .55rem; }
+ .hero-step { width: 3.15rem; height: 3.15rem; }
+ .hero-value { font-size: 2.65rem; }
+ .switch-grid { gap: .5rem; }
+ .switch-card { padding: .7rem; }
+ .switch-title { align-items: flex-start; }
+ .switch-title small { display: none; }
+ .co2-summary { grid-template-columns: auto 1fr; }
+ .co2-summary .quality-chip { grid-column: 1 / -1; justify-self: center; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .hero-step:hover, .theme-button:hover, .open-section:hover, .glass-switch:hover, .open-panel:hover { transform: none; }
+}
diff --git a/web/js/api.js b/web/js/api.js
new file mode 100644
index 0000000..ae0c8c1
--- /dev/null
+++ b/web/js/api.js
@@ -0,0 +1,261 @@
+const query = new URLSearchParams(window.location.search);
+export const isDemo = query.get("demo") === "1";
+
+const demoState = {
+ online: true,
+ running: true,
+ last_seen: new Date().toISOString(),
+ last_error: null,
+ tion: {
+ power: true,
+ heater: true,
+ heating: false,
+ sound: true,
+ mode: "outside",
+ out_temp: 21,
+ in_temp: 20,
+ target_temp: 21,
+ fan_speed: 3,
+ filter_remain: 136.7,
+ device_time: "22:45",
+ request_error_code: 0,
+ model: "S4",
+ light: true,
+ },
+ auto: {
+ available: true,
+ state: "inactive",
+ reason: null,
+ target_speed: null,
+ auto_speed: null,
+ target_heater: null,
+ auto_heater: null,
+ temperature: 25.9,
+ temperature_source: "qingping",
+ last_error: null,
+ config_error: null,
+ },
+ qingping: {
+ online: true,
+ temperature: 25.9,
+ humidity: 69.4,
+ co2: 780,
+ pm25: 0,
+ pm10: 0,
+ battery: 100,
+ last_error: null,
+ },
+ schedule: {
+ available: true,
+ enabled: true,
+ running: true,
+ paused: false,
+ current_action: "set",
+ current_time: "22:45",
+ next_time: "10:00",
+ next_action: "set",
+ override_until_time: "10:00",
+ auto_active: false,
+ auto_fallback_speed: 2,
+ auto_target_temp: 21,
+ scheduled_settings: { speed: 2, target_temp: 21, heater: true },
+ override_active: true,
+ override_until: new Date(Date.now() + 8 * 3600_000).toISOString(),
+ override_settings: { speed: 3 },
+ last_error: null,
+ },
+};
+
+const demoScheduleConfig = {
+ version: 1,
+ enabled: true,
+ timezone: "local",
+ templates: {
+ workday: [
+ { time: "07:30", action: { type: "set", power: true, speed: 3, heater: true, target_temp: 21 } },
+ { time: "09:00", action: { type: "auto", speed: 2, target_temp: 21 } },
+ { time: "22:45", action: { type: "set", speed: 1 } },
+ ],
+ weekend: [
+ { time: "09:30", action: { type: "set", power: true, speed: 3, heater: true, target_temp: 21 } },
+ { time: "11:00", action: { type: "auto", speed: 2, target_temp: 21 } },
+ { time: "23:00", action: { type: "set", speed: 1 } },
+ ],
+ },
+ days: { mon: "workday", tue: "workday", wed: "workday", thu: "workday", fri: "workday", sat: "weekend", sun: "weekend" },
+};
+
+const copy = value => JSON.parse(JSON.stringify(value));
+const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
+
+function mutateDemo(path) {
+ const tion = demoState.tion;
+ const schedule = demoState.schedule;
+ let match;
+
+ if ((match = path.match(/^\/api\/tion\/speed\/(\d)$/))) {
+ tion.fan_speed = Number(match[1]);
+ schedule.override_active = true;
+ schedule.override_until_time ||= schedule.next_time;
+ schedule.override_settings.speed = tion.fan_speed;
+ } else if ((match = path.match(/^\/api\/tion\/temperature\/(\d+)$/))) {
+ tion.target_temp = Number(match[1]);
+ schedule.override_active = true;
+ schedule.override_until_time ||= schedule.next_time;
+ schedule.override_settings.target_temp = tion.target_temp;
+ } else if ((match = path.match(/^\/api\/tion\/(power|heater|sound|light)\/(on|off)$/))) {
+ tion[match[1]] = match[2] === "on";
+ schedule.override_active = true;
+ schedule.override_settings[match[1]] = tion[match[1]];
+ } else if ((match = path.match(/^\/api\/tion\/mode\/(outside|recirculation)$/))) {
+ tion.mode = match[1];
+ schedule.override_active = true;
+ schedule.override_settings.mode = match[1];
+ } else if (path === "/api/schedule/override/clear") {
+ schedule.override_active = false;
+ schedule.override_until = null;
+ schedule.override_until_time = null;
+ schedule.override_settings = {};
+ } else if (path === "/api/schedule/pause") {
+ schedule.paused = true;
+ schedule.override_active = false;
+ schedule.override_until_time = null;
+ } else if (path === "/api/schedule/resume") {
+ schedule.paused = false;
+ }
+
+ demoState.last_seen = new Date().toISOString();
+}
+
+async function request(path, options = {}) {
+ if (isDemo) {
+ await wait(options.method === "POST" ? 220 : 80);
+ if (path === "/api/schedule/config") {
+ if (options.method === "PUT") {
+ const replacement = JSON.parse(options.body);
+ Object.keys(demoScheduleConfig).forEach(key => delete demoScheduleConfig[key]);
+ Object.assign(demoScheduleConfig, replacement);
+ return { ok: true, config: copy(demoScheduleConfig) };
+ }
+ return copy(demoScheduleConfig);
+ }
+ if (options.method === "POST") mutateDemo(path);
+ return copy(demoState);
+ }
+
+ const response = await fetch(path, {
+ cache: "no-store",
+ headers: { Accept: "application/json" },
+ ...options,
+ });
+
+ if (!response.ok) {
+ let message = `Ошибка ${response.status}`;
+ try {
+ const body = await response.json();
+ if (body.detail && typeof body.detail === "object") {
+ message = [body.detail.message, body.detail.error].filter(Boolean).join(": ") || message;
+ } else {
+ message = body.detail || body.message || message;
+ }
+ } catch (_) {}
+ throw new Error(typeof message === "string" ? message : JSON.stringify(message));
+ }
+
+ return response.json();
+}
+
+export const api = {
+ status: () => request("/api/status"),
+ post: path => request(path, { method: "POST" }),
+ scheduleConfig: () => request("/api/schedule/config"),
+ saveScheduleConfig: config => request("/api/schedule/config", {
+ method: "PUT",
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
+ body: JSON.stringify(config),
+ }),
+};
+
+export function configureTheme(button) {
+ const allowed = ["auto", "light", "dark"];
+ const explicit = query.get("theme");
+ let mode = allowed.includes(explicit) ? explicit : (localStorage.getItem("tion-theme") || "auto");
+
+ const apply = () => {
+ const dark = mode === "dark" || (mode === "auto" && matchMedia("(prefers-color-scheme: dark)").matches);
+ document.documentElement.dataset.theme = dark ? "dark" : "light";
+ document.documentElement.dataset.themeMode = mode;
+ if (button) {
+ button.title = `Тема: ${mode === "auto" ? "системная" : mode === "dark" ? "тёмная" : "светлая"}`;
+ button.setAttribute("aria-label", button.title);
+ }
+ };
+
+ apply();
+ matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => mode === "auto" && apply());
+
+ if (button) button.addEventListener("click", () => {
+ mode = allowed[(allowed.indexOf(mode) + 1) % allowed.length];
+ localStorage.setItem("tion-theme", mode);
+ apply();
+ });
+
+ return () => mode;
+}
+
+export function setRangeProgress(input) {
+ const min = Number(input.min);
+ const max = Number(input.max);
+ const value = Number(input.value);
+ input.style.setProperty("--progress", `${((value - min) / (max - min)) * 100}%`);
+}
+
+export function airQuality(co2) {
+ if (!Number.isFinite(Number(co2))) return { label: "Нет данных", tone: "muted", pct: 0 };
+ const value = Number(co2);
+ if (value < 700) return { label: "Отличный воздух", tone: "good", pct: value / 20 };
+ if (value < 900) return { label: "Хороший воздух", tone: "good", pct: value / 20 };
+ if (value < 1300) return { label: "Повышенный CO₂", tone: "warn", pct: value / 20 };
+ if (value < 1600) return { label: "Душно", tone: "warn", pct: value / 20 };
+ return { label: "Нужна вентиляция", tone: "bad", pct: Math.min(100, value / 20) };
+}
+
+export function modeInfo(status) {
+ const schedule = status?.schedule || {};
+ if (!schedule.available) return { label: "Без расписания", detail: "Недоступно", tone: "muted" };
+ if (schedule.paused) return { label: "Ручной режим", detail: "Расписание на паузе", tone: "warn" };
+ if (schedule.override_active) return {
+ label: "Ручное управление",
+ detail: schedule.override_until_time ? `До ${schedule.override_until_time}` : "До следующей точки",
+ tone: "warn",
+ };
+ if (schedule.auto_active) return { label: "AUTO", detail: "По качеству воздуха", tone: "info" };
+ return { label: "Расписание", detail: "Активно", tone: "info" };
+}
+
+export const numberOrDash = (value, digits = 0) => {
+ const number = Number(value);
+ return Number.isFinite(number) ? number.toFixed(digits) : "—";
+};
+
+export function showToast(message, error = false) {
+ let region = document.querySelector(".toast-region");
+ if (!region) {
+ region = document.createElement("div");
+ region.className = "toast-region";
+ document.body.append(region);
+ }
+ const toast = document.createElement("div");
+ toast.className = `toast${error ? " error" : ""}`;
+ toast.textContent = message;
+ region.append(toast);
+ setTimeout(() => toast.remove(), 3200);
+}
+
+export function panelUrl(themeMode = "auto", hash = "") {
+ const url = new URL("/ui/panel", window.location.origin);
+ url.searchParams.set("theme", themeMode);
+ if (isDemo) url.searchParams.set("demo", "1");
+ url.hash = hash;
+ return url.toString();
+}
diff --git a/web/js/panel.js b/web/js/panel.js
new file mode 100644
index 0000000..c2bc4ee
--- /dev/null
+++ b/web/js/panel.js
@@ -0,0 +1,472 @@
+import { api, airQuality, configureTheme, isDemo, modeInfo, numberOrDash, setRangeProgress, showToast } from "./api.js";
+
+const $ = selector => document.querySelector(selector);
+configureTheme($("#theme-button"));
+let state = null;
+let busy = false;
+let scheduleDraft = null;
+let activeTemplate = null;
+
+const weekdays = [
+ ["mon", "Понедельник"],
+ ["tue", "Вторник"],
+ ["wed", "Среда"],
+ ["thu", "Четверг"],
+ ["fri", "Пятница"],
+ ["sat", "Суббота"],
+ ["sun", "Воскресенье"],
+];
+const templateNames = { workday: "Будний", weekend: "Выходной" };
+const clone = value => JSON.parse(JSON.stringify(value));
+
+function templateLabel(name) {
+ return templateNames[name] || name;
+}
+
+if (isDemo) $("#demo-note").hidden = false;
+
+function text(selector, value) { $(selector).textContent = value; }
+function setPill(selector, label, tone) { const element = $(selector); element.textContent = label; element.className = `pill ${tone}`; }
+function toneColor(tone) { return `var(--${tone === "bad" ? "red" : tone === "warn" ? "orange" : tone === "good" ? "green" : tone === "info" ? "blue" : "muted"})`; }
+
+function toggle(selector, active, label = "") {
+ const button = $(selector);
+ button.setAttribute("aria-pressed", String(Boolean(active)));
+ button.textContent = `${label}${label ? " " : ""}${active ? "ON" : "OFF"}`;
+}
+
+function setRange(selector, value) {
+ const input = $(selector);
+ if (Number.isFinite(Number(value))) input.value = value;
+ setRangeProgress(input);
+}
+
+function updateSpeedVisual(value) {
+ const speed = Math.max(1, Math.min(6, Number(value) || 1));
+ text("#speed-value", speed);
+ setRange("#speed-range", speed);
+ document.querySelectorAll(".speed-segments button").forEach((segment, index) => {
+ segment.classList.toggle("active", index < speed);
+ segment.setAttribute("aria-pressed", String(index + 1 === speed));
+ });
+}
+
+function updateTemperatureVisual(value) {
+ const temperature = Math.max(5, Math.min(30, Number(value) || 5));
+ text("#temperature-value", temperature);
+ setRange("#temperature-range", temperature);
+}
+
+function actionLabel(value) {
+ return ({ set: "Настройка режима", auto: "Автоматический режим", off: "Выключение" })[value] || "Точка расписания";
+}
+
+function renderAir(sensor, tion) {
+ const quality = airQuality(sensor.co2);
+ text("#co2", numberOrDash(sensor.co2));
+ text("#air-label", quality.label);
+ text("#air-advice", quality.tone === "good" ? "Проветривание работает нормально" : quality.tone === "warn" ? "Автоматика при необходимости повысит скорость" : quality.tone === "bad" ? "Рекомендуется усилить вентиляцию" : "Ожидаем показания датчика");
+ $("#air-label").style.color = toneColor(quality.tone);
+ $("#air-label").style.background = `var(--${quality.tone === "bad" ? "red" : quality.tone === "warn" ? "orange" : quality.tone === "good" ? "green" : "glass"}-soft)`;
+ $("#co2-progress").style.width = `${Math.max(0, Math.min(100, quality.pct))}%`;
+ setPill("#sensor-status", sensor.online ? "Online" : "Offline", sensor.online ? "good" : "bad");
+ text("#room-temp", numberOrDash(sensor.temperature ?? tion.in_temp, 1));
+ text("#humidity", numberOrDash(sensor.humidity));
+ text("#pm25", numberOrDash(sensor.pm25));
+ text("#pm10", numberOrDash(sensor.pm10));
+ const pm25 = Number(sensor.pm25);
+ const pm10 = Number(sensor.pm10);
+ text("#pm25-quality", Number.isFinite(pm25) ? (pm25 <= 15 ? "Отлично" : pm25 <= 35 ? "Норма" : "Повышено") : "—");
+ text("#pm10-quality", Number.isFinite(pm10) ? (pm10 <= 30 ? "Отлично" : pm10 <= 60 ? "Норма" : "Повышено") : "—");
+}
+
+function renderSchedule(status) {
+ const schedule = status.schedule || {};
+ const mode = modeInfo(status);
+ text("#current-time", schedule.current_time || status.tion?.device_time || "—");
+ text("#next-time", schedule.next_time || "—");
+ text("#current-action", actionLabel(schedule.current_action));
+ text("#next-action", actionLabel(schedule.next_action));
+ text("#mode-title", mode.label);
+ text("#mode-detail", mode.detail);
+ setPill("#schedule-state", schedule.paused ? "Пауза" : schedule.enabled ? "Активно" : "Выкл", schedule.paused ? "warn" : schedule.enabled ? "info" : "muted");
+ const headerMode = schedule.override_active && schedule.override_until_time
+ ? `Ручной до ${schedule.override_until_time}`
+ : mode.label;
+ setPill("#mode-pill", headerMode, mode.tone);
+ $("#mode-banner").className = `mode-banner ${mode.tone === "info" ? "info" : mode.tone === "muted" ? "muted" : "warn"}`;
+ $("#auto-button").disabled = !schedule.available || (!schedule.override_active && !schedule.paused);
+ $("#pause-button strong").textContent = schedule.paused ? "Возобновить расписание" : "Пауза расписания";
+ $("#pause-button small").textContent = schedule.paused ? "Продолжить по расписанию" : "Временно приостановить";
+}
+
+function render(status) {
+ state = status;
+ const tion = status.tion || {};
+ const sensor = status.qingping || {};
+ setPill("#connection", status.online ? "Online" : "Offline", status.online ? "good" : "bad");
+ updateSpeedVisual(tion.fan_speed);
+ updateTemperatureVisual(tion.target_temp);
+ text("#speed-note", status.auto?.state === "active" && status.auto?.target_speed ? `AUTO выбрал скорость ${status.auto.target_speed}` : `Ступень ${numberOrDash(tion.fan_speed)} из 6`);
+ text("#temperature-note", tion.heating ? "Сейчас нагревает входящий воздух" : "Желаемая температура в помещении");
+ toggle("#power-toggle", tion.power);
+ toggle("#heater-toggle", tion.heater);
+ toggle("#sound-toggle", tion.sound, "Звук");
+ toggle("#light-toggle", tion.light, "Свет");
+ text("#power-caption", tion.power ? "Устройство включено" : "Устройство выключено");
+ text("#heater-caption", tion.heating ? "Сейчас нагревает воздух" : "Поддержание температуры");
+ $("#mode-outside").classList.toggle("active", tion.mode === "outside");
+ $("#mode-recirculation").classList.toggle("active", tion.mode === "recirculation");
+ text("#mode-name", tion.mode === "recirculation" ? "Рециркуляция" : "Приточная вентиляция");
+ text("#outside-temp", numberOrDash(tion.out_temp));
+ text("#filter-remain", numberOrDash(tion.filter_remain));
+ const filterPercent = Math.max(0, Math.min(100, Number(tion.filter_remain || 0) / 2));
+ $("#filter-progress").style.width = `${filterPercent}%`;
+ text("#filter-percent", `${Math.round(filterPercent)}%`);
+ text("#model", tion.model || "—");
+ text("#battery", numberOrDash(sensor.battery));
+ renderAir(sensor, tion);
+ renderSchedule(status);
+ text("#updated-at", `Обновлено ${new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}`);
+}
+
+async function refresh(silent = false) {
+ try { render(await api.status()); }
+ catch (error) {
+ setPill("#connection", "Ошибка связи", "bad");
+ if (!silent) showToast(error.message, true);
+ }
+}
+
+async function act(path, source) {
+ if (busy) return;
+ busy = true;
+ source?.setAttribute("disabled", "");
+ source?.setAttribute("aria-busy", "true");
+ try { await api.post(path); await refresh(true); }
+ catch (error) { showToast(error.message, true); await refresh(true); }
+ finally { busy = false; source?.removeAttribute("disabled"); source?.removeAttribute("aria-busy"); }
+}
+
+function stepped(kind, delta, source) {
+ if (busy) return;
+ const isSpeed = kind === "speed";
+ const current = Number(isSpeed ? state?.tion?.fan_speed : state?.tion?.target_temp);
+ const min = isSpeed ? 1 : 5;
+ const max = isSpeed ? 6 : 30;
+ const value = Math.min(max, Math.max(min, current + delta));
+ if (Number.isFinite(value) && value !== current) {
+ if (isSpeed) {
+ state.tion.fan_speed = value;
+ updateSpeedVisual(value);
+ } else {
+ state.tion.target_temp = value;
+ updateTemperatureVisual(value);
+ }
+ act(`/api/tion/${isSpeed ? "speed" : "temperature"}/${value}`, source);
+ }
+}
+
+function optionList(values, current, emptyLabel = "Не менять") {
+ const items = emptyLabel === null ? [] : [["", emptyLabel]];
+ items.push(...values);
+ return items.map(([value, label]) => ``).join("");
+}
+
+function booleanSetting(value) {
+ return value === true ? "on" : value === false ? "off" : "";
+}
+
+function pointMarkup(point, index) {
+ const action = point?.action || {};
+ const type = action.type === "auto" ? "auto" : "set";
+ const speedOptions = Array.from({ length: 6 }, (_, item) => [String(item + 1), String(item + 1)]);
+ const booleanOptions = [["on", "Включить"], ["off", "Выключить"]];
+ return `
+
Текущая ступень вентилятора
Желаемая температура воздуха
Устройство выключено
Поддержание температуры
Ожидаем показания датчика
+