/** Styles of the House Plan card (extracted from the component). */ import { css } from 'lit'; export const cardStyles = css` :host { --hp-bg: var(--card-background-color, #16212e); --hp-line: var(--divider-color, #2b3d4f); --hp-txt: var(--primary-text-color, #e6edf3); --hp-muted: var(--secondary-text-color, #8aa0b3); --hp-accent: var(--primary-color, #3ea6ff); --hp-on: #ffd45c; --hp-open: #ff9f43; /* design tokens (UI chrome only). The icon/plan scale math stays on --icon-size/--dev-size cqw units and never uses these. */ /* spacing scale, fact-based; stray 3/5/7/9/13/14px values are unified onto the nearest step (max +-2px, the whole point of the pass) */ --sp-1: 2px; --sp-2: 4px; --sp-3: 6px; --sp-4: 8px; --sp-5: 12px; --sp-6: 16px; /* px radii of dialogs/buttons/plates (the 22% badge radius is scale math) */ --rad-s: 6px; --rad-m: 8px; --rad-l: 12px; /* font tiers: fine print+labels / body+buttons / title */ --fs-s: 12px; --fs-m: 13px; --fs-l: 15px; /* elevation: badge / floating panel (menu, tip, toast) / dialog */ --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.45); --shadow-2: 0 6px 22px rgba(0, 0, 0, 0.45); --shadow-3: 0 8px 30px rgba(0, 0, 0, 0.5); } ha-card { overflow: visible; /* overflow:hidden breaks position:sticky on the header */ } .empty { padding: 40px 24px; color: var(--hp-txt); text-align: center; display: flex; flex-direction: column; align-items: center; gap: var(--sp-4); } .empty .big { --mdc-icon-size: 56px; color: var(--hp-accent); opacity: 0.7; } .empty .muted { color: var(--hp-muted); font-size: var(--fs-m); margin: 0; } .empty .btn { margin-top: var(--sp-4); } .fixedfloor-loading { animation: fixedfloor-spin 1.1s linear infinite; } .fixedfloor-error p { max-width: 42rem; margin: 0; overflow-wrap: anywhere; } @keyframes fixedfloor-spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .fixedfloor-loading { animation: none; } } .hdr { position: sticky; top: var(--header-height, 56px); z-index: 20; background: var(--card-background-color, var(--hp-bg)); border-radius: var(--ha-card-border-radius, 12px) var(--ha-card-border-radius, 12px) 0 0; } .head { display: flex; align-items: center; gap: 10px; padding: 10px var(--sp-5); border-bottom: 1px solid var(--hp-line); flex-wrap: wrap; } .title { font-size: var(--fs-l); font-weight: 600; display: flex; align-items: center; gap: var(--sp-3); white-space: nowrap; } .title ha-icon { color: var(--hp-accent); --mdc-icon-size: 18px; } .tabs { display: flex; gap: var(--sp-2); background: rgba(127, 127, 127, 0.12); padding: var(--sp-2); border-radius: var(--rad-l); flex-wrap: wrap; } @media (max-width: 620px) { .head { gap: var(--sp-3); padding: var(--sp-4) 10px; } .head .count { display: none; } .head .title { font-size: var(--fs-m); } } .tab { border: 0; background: transparent; color: var(--hp-muted); display: inline-flex; align-items: center; padding: var(--sp-3) var(--sp-5); border-radius: var(--rad-m); font-size: var(--fs-m); font-weight: 600; cursor: pointer; transition: 0.15s; font-family: inherit; } :host([data-pointer-hover]) .tab:hover { color: var(--hp-txt); } .tab.active { background: var(--hp-accent); color: var(--text-primary-color, #fff); } .count { font-size: var(--fs-s); color: var(--hp-muted); } .spacer { flex: 1; } .btn { display: inline-flex; align-items: center; gap: var(--sp-3); border: 1px solid var(--hp-line); background: transparent; color: var(--hp-txt); padding: var(--sp-3) 10px; border-radius: var(--rad-m); cursor: pointer; transition: 0.15s; font-family: inherit; font-size: var(--fs-m); } .btn ha-icon { --mdc-icon-size: 17px; } :host([data-pointer-hover]) .btn:hover { border-color: var(--hp-accent); } .btn.on { background: var(--hp-accent); color: var(--text-primary-color, #fff); border-color: var(--hp-accent); } .btn.ghost { border: none; } .btn[disabled] { opacity: 0.5; pointer-events: none; } .stage.noplan { background: #ffffff; } /* Editors always keep the white drawing sheet — even when a backdrop image is loaded. Without this, a picture plan painted the theme card colour under the grid (owner 2026-08-05). View keeps the historical theme / .noplan split. */ .stage.mode-plan, .stage.mode-devices, .stage.mode-decor { background: #ffffff; } .stage { position: relative; width: 100%; container-type: inline-size; overflow: hidden; touch-action: none; /* custom pinch/pan gestures */ background: var(--ha-card-background, var(--card-background-color, #111)); } .zoomwrap { position: absolute; inset: 0; z-index: 1; } .stage.mode-transition, .stage.mode-transition .zoomwrap { transition: none !important; } .stage.mode-transition .hp-view-only-layer, .stage.mode-transition .hp-editor-only-layer { pointer-events: none; will-change: opacity; } .stage.mode-transition .hp-paper { fill: var(--hp-mode-paper) !important; } /* Four-phase environment (#146): constant layers cross-fade because CSS gradients themselves are not reliably interpolated. Everything stays behind the plan; the plan tree is never dimmed, tinted or faded. */ .hp-day-cycle-env { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; isolation: isolate; } .hp-day-cycle-bg { position: absolute; inset: 0; opacity: 0; overflow: hidden; transition: opacity 1100ms cubic-bezier(.22, .61, .36, 1); } .hp-day-cycle-bg.active { opacity: 1; } .hp-day-cycle-sun { position: absolute; left: var(--hp-day-cycle-sun-x); top: var(--hp-day-cycle-sun-y); width: clamp(140px, 25cqw, 250px); height: clamp(140px, 25cqw, 250px); transform: translate(-50%, -50%); border-radius: 50%; filter: blur(10px); opacity: var(--hp-day-cycle-sun-opacity); transition: left 1100ms cubic-bezier(.22, .61, .36, 1), top 1100ms cubic-bezier(.22, .61, .36, 1), opacity 1100ms cubic-bezier(.22, .61, .36, 1); } .stage.mode-transition .hp-day-cycle-env { transition: none; } .stage.daycycle .hp-paperg, .hp-static-stage.daycycle .hp-paperg { filter: drop-shadow(0 0 1px var(--hp-day-cycle-outline-near)) drop-shadow(0 0 5px var(--hp-day-cycle-outline-mid)) drop-shadow(0 0 10px var(--hp-day-cycle-outline-far)); transition: filter 1100ms cubic-bezier(.22, .61, .36, 1); } @media (prefers-reduced-motion: reduce) { .hp-day-cycle-bg, .hp-day-cycle-sun, .stage.daycycle .hp-paperg, .hp-static-stage.daycycle .hp-paperg { transition: none; } } .sunlayer { pointer-events: none; } /* the compass dial in the general settings (docs/SUN.md) */ .sunrow { display: flex; align-items: center; gap: var(--sp-5); margin: var(--sp-3) 0; } .suncol { flex: 1; display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; } .compass { width: 120px; height: 120px; flex: none; touch-action: none; cursor: grab; user-select: none; } .compass:active { cursor: grabbing; } .compass .cring { fill: rgba(255, 255, 255, 0.04); stroke: var(--divider-color, #444); stroke-width: 2; } .compass .ctick { stroke: var(--secondary-text-color, #9aa4ad); stroke-width: 2; } .compass .ctick.minor { stroke-width: 1; opacity: 0.6; } .compass .cneedle line { stroke: var(--primary-color, #3ea6ff); stroke-width: 2.5; stroke-linecap: round; } .compass .cneedle path { fill: var(--primary-color, #3ea6ff); } .compass .cneedle text { fill: var(--text-primary-color, #fff); font-size: 11px; font-weight: 700; } .compass .cdeg { fill: var(--secondary-text-color, #9aa4ad); font-size: 13px; } .compass.unset .cneedle { opacity: 0.35; } /* HP-1552: first-open boot veil — the plan hides until the stage height settles */ .stage.hpboot .zoomwrap, .stage.hpboot .zoombadge { visibility: hidden; } /* AUD-1552-02: post-veil grace — HA chrome landing after the cap moves the stage height smoothly; the viewport ResizeObserver refits the plan along the transition, so a late panel glides instead of jumping. */ .stage.hpsettle { transition: height 0.25s ease; } .bootveil { position: absolute; inset: 0; z-index: 40; display: flex; align-items: center; justify-content: center; background: var(--hp-bg, #16212e); opacity: 1; transition: opacity 0.15s ease; pointer-events: none; } .bootveil.off { opacity: 0; } .bootveil .boothouse { position: static; /* the main .zoomwrap SVG is pinned to inset:0 — not this icon */ width: 56px; height: 56px; fill: var(--hp-accent); opacity: 0.85; animation: hp-boot-pulse 1.3s ease-in-out infinite; } .recoveryoverlay { position: absolute; inset: 0; /* Above the contextual editor tray (70): recovery is the one state in which every stage-editing surface must be inert. Dialogs live outside the stage and retain their own higher card-level stacking context. */ z-index: 75; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-4); padding: var(--sp-6); box-sizing: border-box; /* The final solid layer guarantees an opaque recovery surface even when a custom HA theme exposes its card colour as rgba(). */ background: linear-gradient(var(--ha-card-background, var(--card-background-color, #111)), var(--ha-card-background, var(--card-background-color, #111))), #111; color: var(--primary-text-color, #fff); text-align: center; pointer-events: auto; transition: opacity 0.15s ease; } .recoveryoverlay.phase-entering, .recoveryoverlay.phase-leaving { opacity: 0; } .recoveryoverlay.phase-fading-in, .recoveryoverlay.phase-opaque { opacity: 1; } .recoveryoverlay ha-icon { --mdc-icon-size: 44px; color: var(--hp-accent); } @keyframes hp-boot-pulse { 0%, 100% { opacity: 0.3; transform: scale(0.94); } 50% { opacity: 0.9; transform: scale(1); } } @media (prefers-reduced-motion: reduce) { .bootveil .boothouse { animation: none; opacity: 0.7; } .recoveryoverlay { transition: none; } .stage.hpsettle { transition: none; } } .zoomctl { display: inline-flex; gap: var(--sp-1); background: rgba(127, 127, 127, 0.12); border-radius: var(--rad-m); padding: var(--sp-1); } .zoomctl .zb { border: none; padding: var(--sp-3) var(--sp-4); } .zoomctl .zb[disabled] { opacity: 0.4; pointer-events: none; } .projection-toggle { min-width: 44px; min-height: 44px; justify-content: center; padding: var(--sp-3); } /* docs/CANVAS.md §4.1: objects an order of magnitude away from the plan do not decide the opening view — one quiet chip says so and offers to take them in. Never a modal (owner). */ .farhint { position: absolute; left: 50%; transform: translateX(-50%); bottom: var(--sp-4); z-index: 12; display: flex; align-items: center; gap: var(--sp-2); max-width: calc(100% - var(--sp-8)); background: var(--card-background-color, var(--hp-bg)); opacity: 0.94; color: var(--hp-txt); border: 1px solid var(--divider-color, #33404d); border-radius: var(--rad-m); padding: var(--sp-1) var(--sp-3); font-size: var(--fs-s); } .farhint ha-icon { --mdc-icon-size: 18px; color: var(--hp-warn, #e2a03f); flex: none; } .farhint span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* docs/CANVAS.md §5: the plane has no edges, so you can pan until nothing is on screen. One pointer home, one click back. */ .homearrow { position: absolute; z-index: 12; transform: translate(-50%, -50%); width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; background: var(--card-background-color, var(--hp-bg)); color: var(--hp-accent); border: 1px solid var(--hp-accent); opacity: 0.9; padding: 0; } .homearrow ha-icon { --mdc-icon-size: 22px; line-height: 1; } .zoombadge { position: absolute; z-index: 12; left: var(--sp-4); bottom: var(--sp-4); background: var(--card-background-color, var(--hp-bg)); opacity: 0.92; color: var(--hp-txt); border: 1px solid var(--hp-accent); border-radius: var(--rad-m); padding: var(--sp-1) var(--sp-4); font-size: var(--fs-s); font-weight: 600; pointer-events: none; } /* Only the plan canvas owns the whole zoom wrapper. Context trays now live inside .stage as well, and may contain small SVG previews (furniture in particular); the old descendant-wide stage-SVG selector stretched every preview over the tray and stacked them into one blocking artefact. The vacuum trail/fit overlays have their own explicit geometry below. */ .zoomwrap > svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; } .plan-svg { z-index: 1; } .iso-underlay-svg { z-index: 0; overflow: visible; } .iso-shadows-svg { z-index: 3; overflow: visible; } .iso-walls-svg { z-index: 4; overflow: visible; } .iso-underlay-svg, .iso-shadows-svg, .iso-walls-svg, .iso-underlay, .iso-shadows, .iso-walls, .iso-openings { pointer-events: none; } .iso-side-hi { stop-color: #b9bdbe; } .iso-side-lo { stop-color: #969c9f; } .iso-top-hi { stop-color: #fafaf7; } .iso-top-lo { stop-color: #e2e4e2; } .iso-wall-side { fill: url(#hp-iso-wall-side) #a8acae; stroke: #92989b; stroke-width: 0.7; vector-effect: non-scaling-stroke; } .iso-wall-top { fill: url(#hp-iso-wall-top) #f3f3f1; stroke: #d7d9d8; stroke-width: 0.8; vector-effect: non-scaling-stroke; } .iso-floor-side { fill: #858b8d; stroke: #71787b; stroke-width: 0.7; vector-effect: non-scaling-stroke; } .iso-opening-panel { fill: #d7d9d7; fill-opacity: 0.96; stroke: #7f878b; stroke-width: 0.9; vector-effect: non-scaling-stroke; } .iso-opening-panel.iso-window { fill: #dfeff4; fill-opacity: 0.72; stroke: #8aa7b1; } .iso-ambient-shadow { fill: rgba(15, 21, 25, 0.22); filter: url(#hp-iso-ambient-shadow); } .iso-contact-shadow { fill: none; stroke: rgba(22, 28, 31, 0.25); stroke-width: 3; filter: url(#hp-iso-contact-shadow); vector-effect: non-scaling-stroke; } .iso-leaf-shadow { fill: none; stroke: rgba(18, 23, 27, 0.24); stroke-width: 4; filter: url(#hp-iso-leaf-shadow); vector-effect: non-scaling-stroke; } @media (prefers-color-scheme: dark) { .iso-side-hi { stop-color: #4c555a; } .iso-side-lo { stop-color: #343c40; } .iso-top-hi { stop-color: #687176; } .iso-top-lo { stop-color: #50585d; } .iso-wall-side { stroke: #30373b; } .iso-wall-top { stroke: #7b858a; } .iso-floor-side { fill: #2d3438; stroke: #20272a; } .iso-opening-panel { fill: #626b70; stroke: #899399; } .iso-opening-panel.iso-window { fill: #75919b; stroke: #abc6ce; } .iso-ambient-shadow { fill: rgba(0, 0, 0, 0.34); } .iso-contact-shadow, .iso-leaf-shadow { stroke: rgba(0, 0, 0, 0.38); } } @media (forced-colors: active) { .iso-wall-side, .iso-wall-top, .iso-floor-side, .iso-opening-panel { fill: Canvas; stroke: CanvasText; forced-color-adjust: auto; } .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } } @supports not (filter: blur(1px)) { .iso-ambient-shadow, .iso-contact-shadow, .iso-leaf-shadow { display: none; } } /* Opaque plan paper (owner 2026-08-03): the scene bg_color / daynight sky shows ONLY around the plan, never through it. The colour is the pre-bg_color canvas — the theme card background under an image plan, plain white for a hand-drawn one (.stage.noplan). On drawn plans the paper is per-room shapes following the room contours — fill only, never a stroke, so the paper cannot poke past a wall. Night dimming comes from the .zoomwrap brightness filter, never from alpha (docs/SUN.md). */ .hp-paper { fill: var(--ha-card-background, var(--card-background-color, #111)); stroke: none; } .stage.noplan .hp-paper, .stage.mode-plan .hp-paper, .stage.mode-devices .hp-paper, .stage.mode-decor .hp-paper { fill: #ffffff; } /* Owner 2026-08-04: «углы границ комнат всё ещё с зубцами». A miter join on a 30-45° corner shoots a spike far past the wall (and flips to an ugly bevel once past the miter limit) — the same defect the decor lines had before they got round caps. Every room border, in EVERY renderer that reuses these styles (plan view, plan editor, static space-card), joins its walls with a ROUND join instead: the corner reads as the stroke's own radius, never as a tooth. The linecap matters only for the open outlines below, but it costs nothing to state it here. */ .room { transition: 0.12s; cursor: default; /* v1.40.1: rooms are not clickable — the label's link icon is */ stroke-linejoin: round; stroke-linecap: round; } .room.overlay { fill: transparent; stroke: transparent; stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); } :host([data-pointer-hover]) .stage.mode-view .room.overlay:not(.styled):hover { stroke: var(--hp-accent); stroke-opacity: 1; } .room.yard { fill: rgba(75, 140, 90, 0.14); stroke: #4b8c5a; stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); } :host([data-pointer-hover]) .stage.mode-view .room.yard:not(.styled):hover { stroke: var(--hp-accent); stroke-opacity: 1; } .room.styled { stroke: var(--room-stroke, transparent); stroke-opacity: var(--room-stroke-op, 0); stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); fill: var(--room-fill, transparent); fill-opacity: var(--room-fill-op, 0); } .glow-base-layer, .glow-base-tunnels, .glow-pools-frame, .glow-pools, .glow-spot { pointer-events: none; } /* The parent isolates all source spots from the room data fill, Glow base, paper and backdrop. A spot is one circle clipped to the floor its lamp can see, so the whole spot screen-blends as a single primitive — there is no mask left to be dropped on a promoted layer. Per-stop alpha already contains the shared 0.7 ceiling; the spot opacity below only animates between 0 and 1 and is never another persistent alpha ceiling. */ .glow-pools-frame, .glow-pools, .glow-spot { isolation: isolate; } .glow-pools.blend-screen .glow-spot { mix-blend-mode: screen; } .glow-spot { opacity: 1; transition: opacity 500ms ease; } .glow-spot.is-entering, .glow-spot.is-leaving { opacity: 0; } @media (prefers-reduced-motion: reduce) { .glow-spot { transition: none; } } /* The explicit late room-hover layer owns the wash and halo. Keeping CSS filters off room paths prevents Chromium from recompositing the sibling screen-blended Glow layer for one bright frame on every hover. */ :host([data-pointer-hover]) .stage.mode-view .room.styled:hover { stroke: var(--hp-accent); stroke-opacity: 1; } /* doors, windows & gates */ .op-leaf { transition: transform 0.6s ease; } .op-arc { stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); transition: stroke-dashoffset 0.6s ease; } /* hover affordance: a rounded outline hugging the wall strip + a grab cursor */ .op-outline { fill: none; stroke: var(--hp-accent); stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1)); opacity: 0; transition: opacity 0.15s; pointer-events: none; } :host([data-pointer-hover]) .stage.markup g.opening:hover .op-outline { opacity: 0.9; } /* openings are pure status graphics outside Plan mode: no cursor, no hover, no hit target — View must not interact with them at all */ .op-hit { fill: transparent; pointer-events: none; cursor: default; } .stage.markup .op-hit { pointer-events: auto; cursor: grab; touch-action: none; /* drags, not scrolls, on touch */ } .stage.markup .op-hit:active { cursor: grabbing; } .stage.markup .opening.orphan { pointer-events: auto; cursor: pointer; color: var(--error-color, #db4437); } .stage.markup .opening.orphan circle { fill: var(--hp-bg); stroke: currentColor; stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); } .stage.markup .opening.orphan text { fill: currentColor; font-weight: 800; font-size: calc(12px * var(--hp-cell-visual-scale, 1)); pointer-events: none; } /* HP-1550-04: in the resize tool the wall handles own the hit test — the transparent .op-hit of a door at the midpoint of a wall used to sit ON TOP of the handle and made that wall ungrabbable for both rooms. Openings are not editable in this tool (they ride along with the wall), so their hit area goes fully inert; every other Plan tool is untouched. */ .stage.markup.tool-resize .op-hit { pointer-events: none; cursor: default; } .oplock { --oplock-size: calc(var(--icon-size, 2.5cqw) * 0.62); --oplock-core-size: calc(var(--oplock-size) / 1.26875); --oplock-stroke-ratio: 0.01875; --oplock-stroke-width: max(1px, calc(var(--oplock-core-size) * var(--oplock-stroke-ratio))); --oplock-core-bg: light-dark(#fff, #252525); --oplock-core-fg: light-dark(#252525, #fff); --oplock-shell-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); --oplock-shell-shadow: 0 calc(var(--oplock-core-size) * .025) calc(var(--oplock-core-size) * .05) rgb(37 40 45 / 12%), 0 calc(var(--oplock-core-size) * .1) calc(var(--oplock-core-size) * .175) calc(var(--oplock-core-size) * -.025) rgb(37 40 45 / 18%); --oplock-core-shadow: 0 0 0 0 transparent; pointer-events: none; /* inert while editing; clickable in View (rule below) */ position: absolute; transform: translate(-50%, -50%); width: var(--oplock-size); height: var(--oplock-size); display: grid; place-items: center; border: 0; background: transparent; z-index: 1; } .oplock.theme-light { --oplock-core-bg: #fff; --oplock-core-fg: #252525; --oplock-shell-stroke: #BCBCBC; } .oplock.theme-dark { --oplock-core-bg: #252525; --oplock-core-fg: #fff; --oplock-shell-stroke: rgb(37 37 37 / 75%); --oplock-core-shadow: inset 0 calc(var(--oplock-core-size) * .0125) calc(var(--oplock-core-size) * .0125) rgb(255 255 255 / 70%); } .stage.mode-view .oplock { pointer-events: auto; cursor: pointer; } .oplock-shell { box-sizing: border-box; width: 100%; height: 100%; border: var(--oplock-stroke-width) solid var(--oplock-shell-stroke); border-radius: 50%; background: transparent; box-shadow: var(--oplock-shell-shadow); display: grid; place-items: center; pointer-events: none; backdrop-filter: none; } .oplock-core { width: var(--oplock-core-size); height: var(--oplock-core-size); border-radius: 50%; background: var(--oplock-core-bg); color: var(--oplock-core-fg); box-shadow: var(--oplock-core-shadow); display: grid; place-items: center; pointer-events: none; } .oplock ha-icon { --mdc-icon-size: calc(var(--oplock-core-size) * 0.55); display: flex; line-height: 0; } .oplock.locked { --oplock-core-bg: #66D17A; --oplock-core-fg: light-dark(#fff, #252525); --oplock-shell-stroke: #66D17A; } .oplock.theme-light.locked { --oplock-core-fg: #fff; } .oplock.theme-dark.locked { --oplock-core-fg: #252525; --oplock-stroke-ratio: .025; } .oplock.unlocked { --oplock-core-bg: #F0410C; --oplock-core-fg: light-dark(#fff, #252525); --oplock-shell-stroke: #F0410C; } .oplock.theme-light.unlocked { --oplock-core-fg: #fff; } .oplock.theme-dark.unlocked { --oplock-core-fg: #252525; --oplock-stroke-ratio: .025; } .oplock.unknown { --oplock-core-fg: var(--hp-muted); } .btn.lockact { width: 100%; justify-content: center; display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-4); } .btn.lockact.warn { color: var(--error-color, #d33); border-color: var(--error-color, #d33); } .oprow { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-3) 0; } .oprow b { margin-left: auto; } .oprow.ok b { color: #66d17a; } .oprow.warn b { color: var(--hp-open); } @media (prefers-reduced-motion: reduce) { .op-leaf, .op-arc { transition: none; } } /* Unified device activity: alarm, short event and continuous state all share this one renderer. No static rings are allowed. */ .dev ha-icon { position: relative; z-index: 1; } .device-pulse { position: absolute; left: 50%; top: 50%; width: var(--dev-size); height: var(--dev-size); transform: translate(-50%, -50%); pointer-events: none; z-index: 0; } .device-pulse i { position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--ripple-color, var(--hp-accent)); opacity: 0; } /* A witnessed edge: exactly three waves over the 3.3 s runtime window. */ .device-pulse.short i { animation: hp-pulse-short 1.1s cubic-bezier(.22,.61,.36,1) 1 forwards; } .device-pulse.short i:nth-child(2) { animation-delay: 1.1s; } .device-pulse.short i:nth-child(3) { animation-delay: 2.2s; } .device-pulse.short.gen2 i { animation-name: hp-pulse-short-b; } .device-pulse.continuous i:nth-child(n + 2), .device-pulse.alarm i:nth-child(n + 3) { display: none; } .device-pulse.continuous i:first-child { animation: hp-pulse-continuous 3.6s cubic-bezier(.45,.05,.55,.95) infinite; } .device-pulse.alarm i { border-width: 3px; border-color: #F0410C; animation: hp-pulse-alarm 2.4s cubic-bezier(.22,.61,.36,1) infinite; } .device-pulse.alarm i:nth-child(2) { animation-delay: 1.2s; } @keyframes hp-pulse-short { 0% { transform: scale(1); opacity: 0.55; } 70% { opacity: 0.22; } 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } } /* Alternate identity: a rapid retrigger restarts the browser timeline. */ @keyframes hp-pulse-short-b { 0% { transform: scale(1); opacity: 0.55; } 70% { opacity: 0.22; } 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } } @keyframes hp-pulse-continuous { 0% { transform: scale(1); opacity: 0.55; } 65% { opacity: 0.18; } 100% { transform: scale(var(--ripple-scale, 1.5)); opacity: 0; } } @keyframes hp-pulse-alarm { 0% { transform: scale(1); opacity: 0.72; } 100% { transform: scale(1.5); opacity: 0; } } .activity-dot { position: absolute; right: 8%; bottom: 8%; width: 18%; height: 18%; min-width: 4px; min-height: 4px; border-radius: 50%; background: var(--ripple-color, var(--hp-accent)); border: 1px solid color-mix(in srgb, var(--hp-bg) 82%, transparent); pointer-events: none; z-index: 3; } .roomlabel { pointer-events: none; /* draggable only in plan mode (rule below) */ position: absolute; transform: translate(-50%, -50%); font-size: calc(var(--rl-icon-size, var(--icon-size, 2.5cqw)) * 0.5 * var(--rl-scale, 1) * var(--rl-font, 1) * var(--rl-space, 1)); font-weight: 700; letter-spacing: 0.04em; white-space: nowrap; cursor: grab; user-select: none; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 0.15em; text-align: center; } .rlname { display: inline-flex; align-items: center; gap: 0.25em; font-size: calc(1em * var(--rl-name, 1)); } /* Space changes keep their direction, but travel only a few percent: the motion explains what changed without making the whole plan fly around. */ @keyframes hp-slide-left { 0% { transform: translateX(4%); opacity: 0.72; } 100% { transform: translateX(0); opacity: 1; } } @keyframes hp-slide-right { 0% { transform: translateX(-4%); opacity: 0.72; } 100% { transform: translateX(0); opacity: 1; } } .zoomwrap.slide-left { animation: hp-slide-left 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } .zoomwrap.slide-right { animation: hp-slide-right 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } @media (prefers-reduced-motion: reduce) { .zoomwrap.slide-left, .zoomwrap.slide-right { animation: none; } } /* The name is the anchor: the label box is centred on the room point, so anything that takes part in its layout SHIFTS THE NAME. The gear button and the metrics hang below as absolutes — the name renders in exactly the same place in view mode and in the plan editor (owner's request), and the button sits at the very bottom of the card. */ /* Standalone, centred on the room, sized from the device icon: icon-size already rescales with the view, so the button zooms WITH the plan instead of keeping a constant screen size (owner's spec). */ .rlgearbtn { --gear-h: calc(var(--icon-size, 2.5cqw) * 0.77); /* owner: half the previous size */ position: absolute; /* dead-centred on the room, both axes (owner's spec) */ transform: translate(-50%, -50%); display: inline-flex; align-items: center; gap: 0.35em; height: var(--gear-h); padding: 0 calc(var(--gear-h) * 0.38); border: 0; border-radius: 999px; background: var(--hp-accent); color: var(--text-primary-color, #fff); font: inherit; font-size: calc(var(--gear-h) * 0.42); font-weight: 600; line-height: 1; white-space: nowrap; cursor: pointer; pointer-events: auto; opacity: 0.92; box-shadow: var(--shadow-1); z-index: 2; } .rlgearbtn { transition: opacity 0.15s, filter 0.15s; } :host([data-pointer-hover]) .rlgearbtn:hover { opacity: 1; filter: brightness(1.18); } .rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; } .rlgear { --mdc-icon-size: 0.9em; display: inline-flex; margin-right: 0.2em; opacity: 0.6; cursor: pointer; pointer-events: auto; } :host([data-pointer-hover]) .rlgear:hover { opacity: 1; } .rlgo { --mdc-icon-size: 0.85em; display: inline-flex; opacity: 0.55; } .stage.mode-view .rlgo { pointer-events: auto; cursor: pointer; } :host([data-pointer-hover]) .stage.mode-view .rlgo:hover { opacity: 1; } .roomlabel .rlmetrics { position: absolute; /* below the name, outside the centring math */ top: calc(100% + 0.15em); left: 50%; transform: translateX(-50%); white-space: nowrap; display: flex; align-items: center; gap: 0.55em; font-size: calc(0.75em * var(--rl-meta, 1)); /* feedback: 0.62 was unreadable on a tablet */ font-weight: 600; letter-spacing: 0.02em; opacity: 0.9; } .roomlabel .rlm { display: inline-flex; align-items: center; gap: 0.12em; } .roomlabel .rlm ha-icon { --mdc-icon-size: 1.05em; display: inline-flex; } .roomlabel .rlm.lit { opacity: 1; } .bindharow { display: flex; align-items: center; gap: var(--sp-5); flex-wrap: wrap; } .bindharow .entcheck { opacity: 0.9; } .dropbtn { display: flex; align-items: center; gap: var(--sp-4); width: 100%; text-align: left; border: 1px solid var(--hp-muted); border-radius: var(--rad-m); background: transparent; color: var(--hp-txt); padding: var(--sp-4) 10px; cursor: pointer; font-family: inherit; font-size: var(--fs-m); margin-top: var(--sp-3); } .dropbtn .ref { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } .dropbtn ha-icon { --mdc-icon-size: 18px; margin-left: var(--sp-2); } .dropbtn.open { border-color: var(--hp-accent); } .droppanel { border: 1px solid var(--hp-accent); border-top: none; border-radius: 0 0 var(--rad-m) var(--rad-m); padding: var(--sp-3); margin-top: -4px; } .ctrlchips { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin: var(--sp-2) 0; } .ctrlchip { display: inline-flex; align-items: center; gap: var(--sp-2); background: var(--hp-accent); color: var(--text-primary-color, #fff); border-radius: var(--rad-l); padding: var(--sp-2) var(--sp-4); font-size: var(--fs-s); } .ctrlchip ha-icon { --mdc-icon-size: 14px; cursor: pointer; } .ctrllist { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-2); } .ctrlopt { display: flex; align-items: center; gap: var(--sp-4); text-align: left; border: 0; background: transparent; color: var(--hp-txt); padding: var(--sp-3) var(--sp-4); border-radius: var(--rad-s); cursor: pointer; font-family: inherit; font-size: var(--fs-m); } :host([data-pointer-hover]) .ctrlopt:hover { background: var(--secondary-background-color, rgba(128,128,128,0.15)); } .ctrlopt .sub { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; } .ctrlopt ha-icon { --mdc-icon-size: 16px; } .ctrlstates { display: flex; flex-direction: column; gap: var(--sp-2); } .ctrlstate { display: inline-flex; align-items: center; gap: var(--sp-3); color: var(--hp-muted); } .ctrlstate.on { color: var(--hp-txt); } .ctrlstate ha-icon { --mdc-icon-size: 15px; } .cardpreview { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); margin: var(--sp-4) 0 var(--sp-1); padding: 10px; border: 1px dashed var(--hp-muted); border-radius: var(--rad-m); } .cardpreview .cpname { font-weight: 700; letter-spacing: 0.04em; } .cardpreview .cpmeta { display: inline-flex; align-items: center; gap: 0.3em; font-weight: 600; opacity: 0.85; } .cardpreview .cpmeta ha-icon { --mdc-icon-size: 1.05em; } .iconauto { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-s); margin: var(--sp-2) 0 0; } .iconauto ha-icon { --mdc-icon-size: 18px; } .iconauto span { flex: 1; } .iconauto .btn { min-height: 32px; padding: 0 var(--sp-3); } .rlhandle { display: none; position: absolute; width: 9px; height: 9px; border-radius: 2px; background: var(--hp-accent); border: 1px solid var(--card-background-color, #fff); z-index: 2; } .rlhandle.tl { left: -6px; top: -6px; cursor: nwse-resize; } .rlhandle.br { right: -6px; bottom: -6px; cursor: nwse-resize; } .rlhandle.tr { right: -6px; top: -6px; cursor: nesw-resize; } .rlhandle.bl { left: -6px; bottom: -6px; cursor: nesw-resize; } :host([data-pointer-hover]) .stage.markup .roomlabel:hover .rlhandle { display: block; } .stage.markup .roomlabel { pointer-events: auto; } .roomlabel:active { cursor: grabbing; } .measurelayer { position: absolute; inset: 0; pointer-events: none; } .alignline { stroke: var(--hp-accent); stroke-width: calc(1.2px * var(--hp-cell-visual-scale, 1)); stroke-dasharray: calc(4px * var(--hp-cell-visual-scale, 1)) calc(4px * var(--hp-cell-visual-scale, 1)); pointer-events: none; opacity: 0.9; } .aligndot { fill: var(--hp-accent); pointer-events: none; } .measurelabel.on45 { color: #4bd28f; border-color: #4bd28f; } .hdr.kioskhide { display: none; } .kioskdots { position: absolute; left: 50%; bottom: var(--sp-5); transform: translateX(-50%); display: flex; gap: var(--sp-4); z-index: 5; pointer-events: none; } .kdot { width: 9px; height: 9px; border-radius: 50%; background: var(--hp-muted); opacity: 0.55; } .kdot.on { background: var(--hp-accent); opacity: 1; } .measurelabel { position: absolute; transform: translate(12px, -150%); font-size: var(--fs-s); font-weight: 600; padding: 1px var(--sp-3); border-radius: var(--rad-s); background: rgba(0, 0, 0, 0.72); color: #fff; white-space: nowrap; pointer-events: none; user-select: none; z-index: 3; } .measurelabel.opdimension { transform: translate( calc(-50% + var(--op-label-shift-x, 0px)), calc(-50% + var(--op-label-shift-y, -12px)) ); border: 1px solid var(--hp-open, #ff9800); } /* decor (background) layer */ .decorlayer .dshape { pointer-events: none; } .stage.mode-decor .decorlayer .dshape { pointer-events: visiblePainted; cursor: pointer; } .stage.mode-decor.dtool-select .decorlayer .dshape { cursor: move; } /* …but a DRAWING tool owns the canvas: existing shapes stop being targets entirely, so a new line can start exactly on the end of an old one instead of grabbing it (owner, 2026-08-04). Same for the picture tool — a shape lying over the plan must not block the picture's own drag. Only select (move) and erase (delete) keep the shapes clickable. */ .stage.mode-decor.dtool-line .decorlayer .dshape, .stage.mode-decor.dtool-rect .decorlayer .dshape, .stage.mode-decor.dtool-ellipse .decorlayer .dshape, .stage.mode-decor.dtool-text .decorlayer .dshape, .stage.mode-decor.dtool-furniture .decorlayer .dshape, .stage.mode-decor.dtool-backdrop .decorlayer .dshape { pointer-events: none; } /* the furniture tool is a stamp: the press must reach the stage even when it lands on a sofa that is already there (docs/FURNITURE.md §4) */ .stage.mode-decor.dtool-furniture { cursor: copy; } /* ONE exception (owner, 2026-08-04): under the TEXT tool an existing LABEL is a target again — pressing it opens its editor instead of starting a new label on top of the old one. Only labels: a line or a rectangle under the text tool stays inert, so the press reaches the stage and a new label is created there. */ .stage.mode-decor.dtool-text .decorlayer .dshape.dtext, .stage.mode-decor.dtool-select .decorlayer .dshape.dtext, .stage.mode-decor.dtool-erase .decorlayer .dshape.dtext { /* A label is one logical decor object. SVG's visiblePainted would hit only the ink of individual glyphs, so spaces, counters and the area inside its selection glow behaved like empty canvas. */ pointer-events: visiblePainted; pointer-events: bounding-box; } .stage.mode-decor.dtool-text .decorlayer .dshape.dtext { cursor: text; } /* Erasing a hairline must not require pixel-perfect aim. The duplicate geometry is invisible and exists only while Erase is active. A non-scaling stroke keeps the target comfortably wide at every zoom. */ .decorlayer .derasehit { fill: none; stroke: transparent; stroke-width: 16px; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; pointer-events: none; } .stage.mode-decor.dtool-erase .decorlayer .derasehit { pointer-events: stroke; } /* A dashed line must remain selectable across its gaps. This proxy also makes the only entry to line-style properties — double click in Select — practical for hairlines at every zoom. */ .decorlayer .dselecthit { fill: none; stroke: transparent; stroke-width: 16px; stroke-linecap: round; vector-effect: non-scaling-stroke; pointer-events: none; } .stage.mode-decor.dtool-select .decorlayer .dselecthit { pointer-events: stroke; cursor: move; } .decorlayer .dsel { filter: drop-shadow(0 0 3px var(--hp-accent)); } .decorlayer .ddraft { stroke-dasharray: calc(6px * var(--hp-cell-visual-scale, 1)) calc(5px * var(--hp-cell-visual-scale, 1)); pointer-events: none; } .decorlayer text { font-weight: 600; user-select: none; dominant-baseline: middle; text-anchor: middle; } .stage.mode-decor { outline: 2px solid #26a69a; outline-offset: -2px; } /* backdrop transform frame (docs/BACKDROP.md §2). Editor chrome: the outline never takes a pointer, the four corner handles do — and they are finger-sized (r = 2 % of the visible view), because this is dragged on a tablet as often as with a mouse. */ .bdframe .bdbox { fill: none; stroke: var(--hp-accent); stroke-width: 2; stroke-dasharray: 10 7; vector-effect: non-scaling-stroke; pointer-events: none; opacity: 0.9; } /* the HIT circle: invisible, finger-sized, the only thing that takes a pointer. The visible bead is .bdknob, a quarter of its radius — the same visual/hit split .dthandle + .dtknob and .rszhandle + .rszicon use. */ .bdframe .bdhandle { fill: transparent; stroke: none; pointer-events: all; touch-action: none; } .bdframe .bdknob { fill: var(--hp-accent); stroke: #fff; stroke-width: 1; vector-effect: non-scaling-stroke; pointer-events: none; } :host([data-pointer-hover]) .bdframe .bdhandle:hover + .bdknob { fill: #fff; stroke: var(--hp-accent); } .bdframe .bd-nwse { cursor: nwse-resize; } .bdframe .bd-nesw { cursor: nesw-resize; } /* the picture itself is the drag target for a move (grab, then grabbing) */ .stage.mode-decor.bdgrab { cursor: grab; } .stage.mode-decor.bdgrabbing, .stage.mode-decor.bdgrabbing .bdframe .bdhandle { cursor: grabbing; } .measurelabel.bdmeasure { transform: translate(-50%, -50%); border: 1px solid var(--hp-accent); } /* the selected decor object's frame — same chrome rules as the backdrop's: the outline never takes a pointer, the handles always do, and they are finger-sized because this is dragged on a tablet too */ .dtframe .dtbox { fill: none; stroke: var(--hp-accent); stroke-width: 1.5; stroke-dasharray: 7 5; vector-effect: non-scaling-stroke; pointer-events: none; opacity: 0.85; } .dtframe .dtstem, .bdframe .dtstem { stroke: var(--hp-accent); stroke-width: 1.5; vector-effect: non-scaling-stroke; pointer-events: none; opacity: 0.85; } /* the HIT circle: invisible, finger-sized, the only thing that takes a pointer. The visible bead is .dtknob, a quarter of its radius — the same visual/hit split .rszhandle + .rszicon use. */ .dtframe .dthandle { fill: transparent; stroke: none; pointer-events: all; touch-action: none; } .dtframe .dtknob { fill: var(--hp-accent); stroke: #fff; stroke-width: 1; vector-effect: non-scaling-stroke; pointer-events: none; } :host([data-pointer-hover]) .dtframe .dthandle:hover + .dtknob { fill: #fff; stroke: var(--hp-accent); } .dtframe .dt-nwse { cursor: nwse-resize; } .dtframe .dt-nesw { cursor: nesw-resize; } .dtframe .dtrot { cursor: grab; } .dtframe .dtendpoint { cursor: crosshair; } .bdframe .dtrot { cursor: grab; } .dtarea { resize: vertical; min-height: 3.4em; font: inherit; line-height: 1.35; } .stage.mode-decor.dtool-line, .stage.mode-decor.dtool-rect, .stage.mode-decor.dtool-ellipse, .stage.mode-decor.dtool-text { cursor: crosshair; } .stage.mode-decor.dtool-erase, .stage.mode-decor.dtool-erase .decorlayer .dshape { cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg transform='rotate(-45 12 12)'%3E%3Crect x='7' y='2' width='10' height='18' rx='2' fill='%23fff' stroke='%23111' stroke-width='1.5'/%3E%3Cpath d='M7 13h10v5a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2z' fill='%23ff9f43' stroke='%23111' stroke-width='1.5'/%3E%3C/g%3E%3C/svg%3E") 5 22, pointer; } .stage.mode-decor .room, .stage.mode-decor .devlayer { pointer-events: none; } .stage.mode-decor .oplock { pointer-events: none; } /* Backdrop-editor de-emphasis is a shared mode-transition coordinate. It multiplies whole presentation groups and never changes Glow source alpha, additive blending, or the underlying resolved state. */ .stage .room, .stage .devlayer, .stage .opening, .stage .room-outline, .stage .wallbodies, .stage .opening-tunnels, .stage .glow-base-layer, .stage .glow-pools-frame, .stage .openwalls { opacity: var(--hp-mode-architecture-opacity, 1); } .decorbar .dcolor { width: 30px; height: 26px; padding: 0; border: none; background: none; cursor: pointer; } .decorbar .dwidth { font-family: inherit; font-size: var(--fs-s); border-radius: var(--rad-s); background: var(--hp-bg2, transparent); color: var(--hp-txt); border: 1px solid var(--hp-muted); padding: var(--sp-2) var(--sp-3); } .decorbar .bdhint { font-size: var(--fs-s); color: var(--hp-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .decorbar .dfill { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-s); cursor: pointer; } .decorbar .dfill input[type="checkbox"] { width: 16px; height: 16px; flex: none; margin: 0; padding: 0; } .decorbar hp-color-opacity { flex: 0 0 auto; } hp-dialog .dfill { display: inline-flex; align-items: center; gap: var(--sp-3); cursor: pointer; } .opening-preview { opacity: 0.5; pointer-events: none; } .opening-preview[data-kind="passage"] { opacity: 1; } .passage-preview-cut { fill: var(--wall-fill, #ffffff); fill-opacity: 0.35; stroke: none; pointer-events: none; } .passage-preview-boundary { stroke: var(--hp-open, #ff9800); stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); pointer-events: none; } .opening-preview .op-leaf, .opening-preview .op-arc { transition: none; } .opening-preview-dot { fill: var(--hp-open, #ff9800); pointer-events: none; } .opening-dimensions, .opening-dimension { pointer-events: none; } .opening-dimension-line, .opening-dimension-tick { fill: none; stroke: var(--hp-open, #ff9800); stroke-width: 1.5; vector-effect: non-scaling-stroke; pointer-events: none; } .opening-dimension-tick { stroke-width: 2; } .stage.edit .room { pointer-events: none; } .stage.markup { cursor: crosshair; } /* room-picking stages: merge (both clicks) and split before a room is chosen */ .stage.markup.tool-merge, .stage.markup.tool-split.pickstage, .stage.markup.tool-delroom { cursor: pointer; } .stage.markup.tool-wallthick { cursor: default; } .stage.markup.tool-wallthick.wallhot { cursor: pointer; } /* Solid wall colour sits under the hatch (owner: both, not either/or). */ .wallbody-fill { fill: var(--wall-fill, #ffffff); fill-opacity: var(--wall-fill-op, 1); fill-rule: evenodd; stroke: none; pointer-events: none; } .wallbody { fill: url(#hp-wall-hatch); fill-rule: evenodd; stroke: var(--room-stroke, var(--hp-muted)); stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1)); pointer-events: none; } /* Thin-on-screen: hatch collapses into noise — fill alone, stroke from fill path's sibling. */ .wallbody.solid { fill: none; } .wallthick-hover { fill: var(--accent-color, #03a9f4); fill-opacity: 0.38; stroke: var(--accent-color, #03a9f4); stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); stroke-linejoin: round; stroke-linecap: round; pointer-events: none; } .wallthick-dlg { position: absolute; z-index: 40; min-width: 200px; transform: translate(-50%, 8px); padding: 10px 12px; border-radius: 10px; background: var(--card-background-color, #fff); box-shadow: 0 8px 28px rgba(0,0,0,.22); display: flex; flex-direction: column; gap: 8px; pointer-events: auto; } .wallthick-hover.isopen { fill: var(--error-color, #f44336); stroke: var(--error-color, #f44336); } .wallthick-dlg .row { display: flex; align-items: center; gap: 8px; } .wallthick-dlg input[type="number"] { width: 5.5em; padding: 4px 6px; border: 1px solid var(--divider-color, #ccc); border-radius: 6px; background: var(--input-fill, transparent); color: var(--primary-text-color); } /* Boundary is neutral off-target, then advertises its resolved action. */ .stage.markup.tool-boundary { cursor: default; } .stage.markup.tool-boundary.boundary-solid { cursor: crosshair; } .stage.markup.tool-boundary.boundary-open { cursor: pointer; } .stage.markup.tool-boundary.boundary-invalid { cursor: not-allowed; } .openwall { stroke: var(--ow-stroke, var(--hp-muted)); stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); stroke-dasharray: calc(7px * var(--hp-cell-visual-scale, 1)) calc(7px * var(--hp-cell-visual-scale, 1)); stroke-linecap: butt; pointer-events: none; opacity: 0.9; } /* Rooms with open/thick stretches: the polygon's own stroke is fully off. The trimmed .room-outline draws normal walls; View hover gets its own top overlay after the wall bodies. */ .room.noedge { stroke-opacity: 0 !important; } /* rooms with open boundaries draw their walls as separate M..L subpaths, so a corner between two of them is two stroke ENDS meeting: round caps fill it in the same way a round join fills a closed contour's corner (owner 2026-08-04 — no teeth anywhere on a room border). */ .room-outline { fill: none; stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); stroke-linejoin: round; stroke-linecap: round; pointer-events: none; } /* Plan editor: trimmed outlines use the markup blue */ .room-outline.outlined { stroke: rgba(62, 166, 255, 0.55); stroke-opacity: 1; } /* The wash is rendered directly above the resolved room fill but below tunnels, Glow, sun and walls. Black at 22% reproduces the old brightness(.78) contract without filtering/promoting an SVG ancestor or changing the room's hue. The halo/outline are rendered again late, above the wall bodies. */ .room-hover-fill { fill: #000; fill-opacity: 0.22; pointer-events: none; } .room-hover-halo { fill: none; stroke: var(--hp-accent); stroke-opacity: 0.28; stroke-width: calc(8px * var(--hp-cell-visual-scale, 1)); stroke-linejoin: round; stroke-linecap: round; pointer-events: none; } .room-hover-outline { fill: none; stroke: var(--hp-accent); stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); stroke-linejoin: round; stroke-linecap: round; pointer-events: none; } .openwall-preview { stroke: #ffc14d; stroke-width: calc(5px * var(--hp-cell-visual-scale, 1)); stroke-dasharray: calc(7px * var(--hp-cell-visual-scale, 1)) calc(7px * var(--hp-cell-visual-scale, 1)); stroke-linecap: round; pointer-events: none; opacity: 0.95; } .openwall-preview.boundary-range.invalid { stroke: var(--error-color, #f44336); } .openwall-preview.boundary-restore { fill: rgba(255, 193, 77, 0.28); fill-rule: evenodd; stroke: #ffc14d; stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); stroke-dasharray: none; stroke-linejoin: round; } .boundary-point { fill: #ffc14d; stroke: #24262d; stroke-width: 1.5; vector-effect: non-scaling-stroke; pointer-events: none; } .boundary-point.invalid circle { fill: var(--error-color, #f44336); } .boundary-point.invalid path { fill: none; stroke: #fff; stroke-width: 1.7; vector-effect: non-scaling-stroke; } .stage.markup .room { pointer-events: none; } .stage.markup .devlayer .dev { display: none; /* in plan mode the icons do not get in the way; labels stay */ } /* mode frames: the edit modes are visible at a glance */ .stage.mode-plan { outline: 2px solid #ffc14d; outline-offset: -2px; } .stage.mode-devices { outline: 2px solid var(--hp-accent); outline-offset: -2px; } .modes { display: inline-flex; gap: var(--sp-1); background: rgba(127, 127, 127, 0.12); border-radius: var(--rad-l); padding: var(--sp-2); } .modetab { display: inline-flex; align-items: center; gap: var(--sp-3); border: 0; background: transparent; color: var(--hp-muted); padding: var(--sp-3) 10px; /* 10px h-padding kept: +2px would wrap the header modes row */ border-radius: var(--rad-m); font-size: var(--fs-m); font-weight: 600; cursor: pointer; font-family: inherit; transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease; } .modetab:active { transform: scale(0.97); } .modetab ha-icon { --mdc-icon-size: 15px; } /* issue #220: a tab can be dragged to a new position in the editors */ .tab[data-reorderable] { cursor: grab; } .tab.dragging { cursor: grabbing; opacity: 0.55; } .tab.droptarget { box-shadow: inset 2px 0 0 0 var(--primary-color, #03a9f4); } .modetab .closex { --mdc-icon-size: 13px; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; min-width: 24px; min-height: 24px; /* Keep the old 13 × 13 flex footprint (+2 px left margin) while the pointer target grows around it. Header width, height and wrapping do not move; only near-misses that used to hit the active tab reach X. */ margin: -5.5px -5.5px -5.5px -3.5px; opacity: 0.75; cursor: pointer; border-radius: var(--rad-s); } :host([data-pointer-hover]) .modetab .closex:hover { opacity: 1; } .editbar .barclose { box-sizing: border-box; width: 40px; height: 40px; padding: 0; margin: 0; min-width: 40px; min-height: 40px; justify-content: center; gap: 0; line-height: 0; } .editbar .barclose ha-icon { flex: none; margin: 0; } .modetab.active { background: var(--hp-accent); color: var(--text-primary-color, #fff); } @media (max-width: 720px) { .modetab .ml { display: none; } } .room.outlined { stroke: rgba(62, 166, 255, 0.55); fill: rgba(62, 166, 255, 0.06); } /* AFTER .outlined: same specificity — source order decides (gotcha x4) */ .room.picked { stroke: #ffc14d; stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); fill: rgba(255, 193, 77, 0.25); } /* Owner 2026-08-04: the grid is a HINT, not content — at full strength the dots argued with the plan on white paper. Both levels are muted, the hierarchy is kept (major still denser than fine). */ .griddot { fill: var(--hp-accent); opacity: 0.35; stroke: rgba(0, 0, 0, 0.35); stroke-width: 0.4; } /* docs/CANVAS.md §7: every coarse node (5x/10x the live step) keeps a bigger, more opaque dot, so zoomed far out the grid still reads as a grid instead of a grey wash. */ .griddot.major { opacity: 0.5; stroke-width: 0; } /* the contour being drawn in the Plan editor: each wall is its own , so the round cap IS the corner (matches the finished .room border) */ .seg { stroke: var(--hp-accent); stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1)); stroke-linecap: round; stroke-linejoin: round; } .pathline { stroke: #ffc14d; stroke-width: calc(3px * var(--hp-cell-visual-scale, 1)); fill: none; stroke-linecap: round; stroke-linejoin: round; } .preview { stroke: #ffc14d; stroke-width: calc(2px * var(--hp-cell-visual-scale, 1)); stroke-dasharray: calc(6px * var(--hp-cell-visual-scale, 1)) calc(5px * var(--hp-cell-visual-scale, 1)); opacity: 0.7; } .active-axis { stroke: #ffc14d; stroke-width: 2; stroke-linecap: round; vector-effect: non-scaling-stroke; pointer-events: none; } .active-vertex { fill: #ffc14d; stroke: #171006; stroke-width: 2; vector-effect: non-scaling-stroke; pointer-events: none; } .wall-repair-preview { stroke: #d93025; stroke-width: 4; stroke-linecap: round; stroke-dasharray: 5 4; vector-effect: non-scaling-stroke; pointer-events: none; } .drawwall-preview-fill { fill: var(--wall-fill, #ffffff); fill-opacity: calc(var(--wall-fill-op, 1) * 0.55); fill-rule: evenodd; stroke: none; pointer-events: none; } .drawwall-preview { fill: url(#hp-wall-hatch); fill-opacity: 0.55; fill-rule: evenodd; stroke: var(--accent-color, #03a9f4); stroke-width: calc(0.5px * var(--hp-cell-visual-scale, 1)); stroke-opacity: 0.7; pointer-events: none; } .vertex { fill: #ffc14d; stroke: #4a2800; stroke-width: calc(1px * var(--hp-cell-visual-scale, 1)); } .vertex.first { fill: #4bd28f; stroke: #04121f; } .plan-snap-overlay, .plan-snap-overlay * { pointer-events: none; } .plan-snap-line { fill: none; stroke: color-mix(in srgb, var(--hp-accent) 82%, white 18%); /* Explicit non-scaling-stroke in the SVG keeps this one screen pixel. */ stroke-width: 1; stroke-linecap: round; opacity: 0.92; } .plan-snap-node { fill: var(--ha-card-background, var(--card-background-color, #fff)); stroke: color-mix(in srgb, var(--hp-accent) 88%, #07131c 12%); stroke-width: 1; vector-effect: non-scaling-stroke; } .plan-snap-node.active { fill: #ffc14d; stroke: #171006; stroke-width: 2; } .plan-snap-node.active.dynamic { fill: #4bd28f; stroke: #04121f; } .plan-snap-node.conflict { fill: #fff; stroke: #d93025; stroke-width: 3; } @media (prefers-color-scheme: dark) { .plan-snap-line { stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%); } .plan-snap-node { fill: #17242c; stroke: #9bdcf5; } .plan-snap-node.active { fill: #ffc14d; stroke: #fff4d6; } .plan-snap-node.active.dynamic { fill: #4bd28f; stroke: #eafff4; } } @media (forced-colors: active) { .plan-snap-line { stroke: CanvasText; opacity: 1; forced-color-adjust: auto; } .plan-snap-node { fill: Canvas; stroke: CanvasText; forced-color-adjust: auto; } .plan-snap-node.active, .plan-snap-node.active.dynamic { fill: Highlight; stroke: HighlightText; forced-color-adjust: auto; } } .areasel, .namein { background: var(--hp-bg); border: 1px solid var(--hp-line); color: var(--hp-txt); border-radius: var(--rad-s); padding: var(--sp-3) var(--sp-4); font-size: var(--fs-m); font-family: inherit; } .namein { width: 130px; } .dev.valonly { /* Saved coordinates remain the centre of a Text shell. The shell may expand, while the invisible anchor stays one core diameter wide. */ width: var(--dev-size, var(--icon-size, 2.5cqw)); } .dev.valonly .device-core { width: max-content; min-width: var(--dev-size, var(--icon-size, 2.5cqw)); padding-inline: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16); border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) / 2); } .dev .valtext { overflow: visible; font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); font-weight: 600; white-space: nowrap; } /* RGB lights: the bulb takes the light's actual color */ /* v1.52.0: the RGB tint of the icon/border is gone — a lamp's colour lives ONLY in its glow spot (owner's rule). The ripple-color fallback keeps using the light colour; that is set inline via --ripple-color. */ /* Sun wedges (docs/SUN.md). The layer is present ONLY above the 3° threshold; crossing it fades the whole layer in or out over EXACTLY 2 s (owner 2026-08-03 — RAY_FADE_MS in src/sun.ts must match). The geometry is untouched: this is a plain opacity animation on the group, so overlapping wedges keep their own blending while it plays. */ .sunlayer { animation: hp-sunfade-in 2s linear both; } .sunlayer.out { animation: hp-sunfade-out 2s linear both; } @keyframes hp-sunfade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes hp-sunfade-out { from { opacity: 1; } to { opacity: 0; } } @media (prefers-reduced-motion: reduce) { /* no fade at all: the rays are simply there or simply gone */ .sunlayer, .sunlayer.out { animation: none; } .sunlayer.out { opacity: 0; } } @media (prefers-reduced-motion: reduce) { .device-pulse { display: none; } } .dev .newdot { position: absolute; top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); right: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.12); width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.34); border-radius: 50%; background: #f0301f; border: 2px solid var(--card-background-color, var(--hp-bg)); pointer-events: none; z-index: 2; } .devlayer { position: absolute; inset: 0; pointer-events: none; z-index: 6; } .dev { position: absolute; /* The surface resolves compatibility icon_size units once. The shared face receives an effective base and only applies the per-device size. */ --dev-size: calc(var(--device-base-size, 2.25cqw) * var(--dev-scale, 1)); /* 101.5/80 is the package shell/core ratio, including its stroke. */ --device-shell-size: calc(var(--dev-size) * 1.26875); --device-shell-inset: calc(var(--dev-size) * 0.134375); --device-shell-stroke-ratio: 0.01875; --device-shell-border-width: max(1px, calc(var(--dev-size) * var(--device-shell-stroke-ratio))); --device-core-bg: var(--card-background-color, #fff); --device-core-fg: var(--primary-text-color, #252525); --device-core-bg: light-dark(#fff, #252525); --device-core-fg: light-dark(#252525, #fff); --device-face-bg: var(--device-core-bg); --device-face-fg: var(--device-core-fg); --device-shell-base-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%)); --device-shell-stroke: var(--device-shell-base-stroke); --device-shell-shadow: 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); --device-core-inset-shadow: 0 0 0 0 transparent; --device-ring-color: transparent; --device-ring-width: 0px; /* The saved point is the exact centre of the icon core. */ width: var(--dev-size); height: var(--dev-size); margin: calc(var(--dev-size) / -2) 0 0 calc(var(--dev-size) / -2); border: 0; background: transparent; display: block; color: var(--device-core-fg); cursor: pointer; pointer-events: auto; transition: opacity 0.2s; box-shadow: none; outline: none; z-index: 2; } .dev.theme-light { --device-core-bg: #fff; --device-core-fg: #000; --device-shell-base-stroke: #BCBCBC; --device-shell-stroke: var(--device-shell-base-stroke); --device-shell-shadow: 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%), 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); --device-core-inset-shadow: 0 0 0 0 transparent; } .dev.theme-dark { --device-core-bg: #252525; --device-core-fg: #fff; --device-shell-base-stroke: rgb(37 37 37 / 75%); --device-shell-stroke: var(--device-shell-base-stroke); --device-shell-shadow: 0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .0375) rgb(37 40 45 / 12%), 0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175) calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%); --device-core-inset-shadow: inset 0 calc(var(--dev-size) * 0.0125) calc(var(--dev-size) * 0.0125) rgb(255 255 255 / 70%); } .dev::before { content: ''; position: absolute; left: 50%; top: 50%; width: max(44px, var(--device-shell-size)); height: max(44px, var(--device-shell-size)); transform: translate(-50%, -50%); border-radius: 50%; pointer-events: auto; z-index: 3; } .device-shell { position: absolute; box-sizing: border-box; display: flex; align-items: center; gap: calc(var(--dev-size) * 0.1); padding: 0; min-width: var(--dev-size); min-height: var(--dev-size); border: 0; background: transparent; box-shadow: none; transition: opacity .2s; pointer-events: none; } .device-shell-frame { position: absolute; z-index: 0; box-sizing: border-box; inset: calc(var(--device-shell-inset) / -1); border: var(--device-shell-border-width) solid var(--device-shell-stroke); /* A saturating radius is resolved from the final border box. Using a second fractional length here lets border-box and radius quantise on different sides of a device pixel at some zoom/DPR combinations. */ border-radius: 9999px; background: transparent; box-shadow: var(--device-shell-shadow); transition: border-color .15s, box-shadow .15s, opacity .2s; pointer-events: auto; /* Normative production fallback: never add a per-marker backdrop blur. */ backdrop-filter: none; } .device-shell:not(.with-values) { left: 0; top: 0; } .device-shell:not(.with-values):not(.text-shell) .device-shell-frame { border-radius: 50%; } .device-shell.with-values.pos-right { left: 0; top: 50%; transform: translateY(-50%); } .device-shell.with-values.pos-left { right: 0; top: 50%; transform: translateY(-50%); flex-direction: row-reverse; } .device-shell.with-values.pos-bottom { left: 50%; top: 0; transform: translateX(-50%); flex-direction: column; } .device-shell.with-values.pos-top { left: 50%; bottom: 0; transform: translateX(-50%); flex-direction: column-reverse; } .device-core { position: relative; z-index: 1; box-sizing: border-box; flex: 0 0 auto; width: var(--dev-size); height: var(--dev-size); display: flex; align-items: center; justify-content: center; border-radius: 50%; background: var(--device-face-bg); color: var(--device-face-fg); box-shadow: var(--device-core-inset-shadow), 0 0 0 var(--device-ring-width) var(--device-ring-color); line-height: 0; transition: background .15s, color .15s, box-shadow .15s, opacity .2s; pointer-events: none; } .device-sections { display: flex; flex-direction: column; align-items: stretch; justify-content: center; gap: calc(var(--dev-size) * .08); min-width: 0; pointer-events: none; } .device-shell.pos-top .device-sections, .device-shell.pos-bottom .device-sections { flex-direction: row; } .dev ha-icon { /* from --dev-size, NOT --icon-size: the per-device size multiplier must scale the GLYPH with its badge. Pinned to the base size, "make this icon bigger" grew an empty box around a default-size glyph (user report via the owner, 2026-07-29). */ --mdc-icon-size: calc(var(--dev-size, var(--device-base-size, 2.25cqw)) * 0.55); display: flex; align-items: center; justify-content: center; line-height: 0; } .stage.mode-devices .dev { cursor: grab; } .stage.mode-devices .dev:active { cursor: grabbing; } .dev.on { --device-face-bg: #F0A00C; --device-face-fg: light-dark(#fff, #252525); --device-shell-stroke: #F0A00C; } .dev.theme-light.on { --device-face-fg: #fff; } .dev.theme-dark.on { --device-face-fg: #252525; --device-shell-stroke-ratio: .0375; } .dev.open { --device-face-bg: var(--hp-open); --device-face-fg: light-dark(#fff, #252525); } .dev.theme-light.open { --device-face-fg: #fff; } .dev.theme-dark.open { --device-face-fg: #252525; } .dev.lock-locked { --device-face-bg: #66D17A; --device-face-fg: light-dark(#fff, #252525); --device-shell-stroke: #66D17A; } .dev.theme-light.lock-locked { --device-face-fg: #fff; } .dev.theme-dark.lock-locked { --device-face-fg: #252525; --device-shell-stroke-ratio: .025; } .dev.lock-unlocked { --device-face-bg: #F0410C; --device-face-fg: light-dark(#fff, #252525); --device-shell-stroke: #F0410C; } .dev.theme-light.lock-unlocked { --device-face-fg: #fff; } .dev.theme-dark.lock-unlocked { --device-face-fg: #252525; --device-shell-stroke-ratio: .025; } /* Interaction wins ordinary state colours. Alarm keeps priority through the more-specific rule below. Unavailable has no visual hover. */ :host([data-pointer-hover]) .dev:not(.unavail):hover { --device-face-bg: #0C82F0; --device-face-fg: light-dark(#fff, #252525); --device-shell-stroke: var(--device-shell-base-stroke); } :host([data-pointer-hover]) .dev.theme-light:not(.unavail):hover { --device-face-fg: #fff; } :host([data-pointer-hover]) .dev.theme-dark:not(.unavail):hover { --device-face-fg: #252525; } :host([data-pointer-hover]) .dev:hover, .dev:focus-visible { z-index: 5; } .dev.unavail { opacity: 0.35; --device-face-bg: #B5BAC1; --device-shell-stroke: var(--device-shell-base-stroke); } .physical-hit { fill: transparent; stroke: transparent; pointer-events: none; cursor: grab; } .stage.tool-select .physical-hit { pointer-events: all; } .physical-hit:active { cursor: grabbing; } line.physical-hit { cursor: pointer; } .physical-hit.selected { fill: rgba(255, 193, 77, 0.24); stroke: transparent; } line.physical-hit.selected { stroke: transparent; } .physical-drag { fill: rgba(255, 193, 77, 0.38); stroke: #ffc14d; stroke-width: 2.5; vector-effect: non-scaling-stroke; pointer-events: none; } .physical-chrome { pointer-events: none; } .physical-chrome .frame, .physical-chrome .stem { fill: none; stroke: #ffc14d; stroke-width: 2; vector-effect: non-scaling-stroke; } .physical-chrome .frame { fill: rgba(255, 193, 77, 0.22); } .physical-chrome polyline.frame { fill: none; } .physical-chrome .move-dot { fill: #ffc14d; stroke: #24262d; stroke-width: 1.5; vector-effect: non-scaling-stroke; } .physical-chrome .rotate-handle { fill: #24262d; stroke: #ffc14d; stroke-width: 2.5; vector-effect: non-scaling-stroke; pointer-events: all; cursor: crosshair; } .drawwall.invalid input { border-color: var(--error-color, #db4437); } .drawwall .rangehint { margin-inline-start: 4px; font-size: 0.78em; opacity: 0.72; } .drawwall.invalid .rangehint { color: var(--error-color, #db4437); opacity: 1; } .stage.markup.tool-partition, .stage.markup.tool-column { cursor: crosshair; } .dev.virtual .device-shell-frame { border-style: dashed; } /* "hide from plan" flag, shown only in the device editor with the "show hidden devices" toggle on (docs/FILTERING.md). BLUE, so a hidden device cannot be mistaken for an unavailable one (translucent dark) — and no live-state paint at all: a ghost is configuration, not status (owner's request). */ .dev.ghost { opacity: 0.6; color: var(--hp-accent); } .dev.ghost .device-shell-frame { border-style: dashed; border-color: var(--hp-accent); box-shadow: none; } .dev.ghost .device-core { background: rgba(62, 166, 255, 0.22); /* fallback for old WebViews */ background: color-mix(in srgb, var(--hp-accent) 30%, var(--card-background-color, #1c2530)); color: var(--hp-accent); } /* HA-disabled is not a user hide: neutral grey + a power-off badge keeps the two ghosts distinguishable without relying on colour alone. */ .dev.ghost.ha-disabled { opacity: 0.62; color: var(--secondary-text-color, #9aa0aa); } .dev.ghost.ha-disabled .device-shell-frame { border-color: var(--secondary-text-color, #9aa0aa); } .dev.ghost.ha-disabled .device-core { background: rgba(120, 124, 134, 0.2); background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 24%, var(--card-background-color, #1c2530)); color: var(--secondary-text-color, #9aa0aa); } .dev .habadge { position: absolute; right: -18%; bottom: -18%; display: grid; place-items: center; width: 46%; height: 46%; min-width: 12px; min-height: 12px; border-radius: 50%; background: var(--card-background-color, #242832); border: 1px solid currentColor; } .dev .habadge ha-icon { width: 72%; height: 72%; } .habindingbanner { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4); border: 1px solid var(--warning-color, #ff9800); border-radius: var(--rad-m); background: color-mix(in srgb, var(--warning-color, #ff9800) 12%, transparent); color: var(--primary-text-color, #f1f3f6); } .habindingbanner > span { flex: 1; min-width: 0; } .habindingbanner > ha-icon { color: var(--warning-color, #ff9800); flex: 0 0 auto; } .habindingbanner.limited { border-color: var(--secondary-text-color, #9aa0aa); background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 10%, transparent); } .habindingbanner.limited > ha-icon { color: var(--secondary-text-color, #9aa0aa); } .dev.sel { --device-ring-color: #F0A00C; --device-ring-width: calc(var(--dev-size) * 0.0375); } .dev:focus-visible { --device-ring-color: #0C82F0; --device-ring-width: calc(var(--dev-size) * 0.0375); } .dev:not(.on):not(.open):not(.alarm):not(.lock-locked):not(.lock-unlocked):not(.unavail):focus-visible { --device-face-fg: #0C82F0; } /* Alert stays above focus, selection, hover and ordinary semantic paint. */ .dev.alarm, :host([data-pointer-hover]) .dev.alarm:hover, .dev.alarm:focus-visible { --device-face-bg: #F0410C; --device-face-fg: light-dark(#fff, #252525); --device-shell-stroke: #F0410C; } .dev.theme-light.alarm { --device-face-fg: #fff; } .dev.theme-dark.alarm { --device-face-fg: #252525; --device-shell-stroke-ratio: .025; } .dev .value-badge { position: relative; z-index: 2; box-sizing: border-box; width: max-content; min-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875); display: flex; align-items: center; justify-content: center; overflow: visible; background: var(--device-core-bg); border: 0; border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.39375); padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14); font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45)); font-weight: 600; line-height: 1; color: var(--device-core-fg); white-space: nowrap; pointer-events: none; } .dev .value-badge.unavailable, .dev .value-badge.missing { opacity: 0.66; } .dev .lqi { position: absolute; top: calc(50% + var(--device-shell-size) / 2 + var(--dev-size) * .05); left: 50%; transform: translateX(-50%); margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.05); font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.38); font-weight: 600; line-height: 1; text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9); white-space: nowrap; pointer-events: none; } .dev .lqi.below-value-badge { margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.8875); } .editbar { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: stretch; border-bottom: 1px solid var(--hp-line); font-size: var(--fs-m); } .editbar-tools { display: flex; align-items: center; gap: 10px; padding: var(--sp-4) var(--sp-5); flex-wrap: wrap; min-width: 0; outline: none; } .editbar-end { display: flex; align-items: center; padding: var(--sp-4) var(--sp-5); border-inline-start: 1px solid var(--hp-line); background: var(--card-background-color, var(--hp-bg)); position: relative; z-index: 2; } .tab .tabedit { --mdc-icon-size: 13px; display: inline-flex; align-items: center; margin-left: var(--sp-3); opacity: 0.4; } :host([data-pointer-hover]) .tab:hover .tabedit { opacity: 0.9; } .tab.tabadd { padding: var(--sp-3) var(--sp-4); } .tab.tabadd ha-icon { --mdc-icon-size: 15px; } .srcrow { display: flex; align-items: center; gap: var(--sp-4); font-size: var(--fs-m); cursor: pointer; padding: var(--sp-1) 0; } .dispsection { margin-top: var(--sp-5) !important; padding-top: var(--sp-4); border-top: 1px solid var(--hp-line); font-weight: 600; color: var(--hp-txt) !important; } .colorrow { display: flex; align-items: center; gap: var(--sp-4); } .gsrow > hp-color-opacity { min-width: min(100%, 210px); justify-content: space-between; } .ripple-colorrow > hp-color-opacity { width: 100%; justify-content: space-between; } .ripple-sizerow > .opl { min-width: 0; } .colorrow input[type='range'] { flex: 1; } .colorrow .tempin { width: 70px; flex: none; } .temprange { display: inline-flex; align-items: center; gap: var(--sp-3); margin-left: auto; color: var(--hp-muted); font-size: var(--fs-s); } /* beat the generic hp-dialog .body .namein { width:100% } rule */ hp-dialog .body .temprange .tempin { width: 56px; flex: none; padding: var(--sp-2) var(--sp-3); } hp-dialog .body .colorrow .tempin { width: 72px; flex: none; } .srcrow { flex-wrap: nowrap; } /* native HA controls (rendered only when the HA frontend defines them; old HA and the smoke env keep the plain inputs). ha-switch is taller than a checkbox - cap its footprint so .srcrow keeps its rhythm. */ .srcrow ha-switch { flex: none; } .colorrow ha-slider { flex: 1; min-width: 0; } .srcrow > span:first-of-type { white-space: nowrap; } .colorrow .opl { color: var(--hp-muted); font-size: var(--fs-s); } .colorrow .opv { font-size: var(--fs-s); min-width: 34px; text-align: right; } .markerlightgroup { min-width: 0; margin: var(--sp-5) 0 0; padding: var(--sp-4); border: 1px solid var(--hp-line); border-radius: var(--rad-m); } .markerlightgroup legend { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-1); padding: 0 var(--sp-2); color: var(--hp-txt); font-weight: 600; } .markerlightgroup legend > span { min-width: 0; overflow-wrap: anywhere; } .markerlightgroup[disabled] > :not(legend) { opacity: .62; } .markerhelpfield { margin-top: var(--sp-4); } .markerhelplabel { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-1); margin-bottom: var(--sp-1); } .markerhelplabel > label { min-width: 0; overflow-wrap: anywhere; } .markerradios { display: grid; gap: var(--sp-1); min-width: 0; } .markerlightgroup .srcrow > span:first-of-type { min-width: 0; white-space: normal; overflow-wrap: anywhere; } .markerglowvalue { margin: var(--sp-3) 0; flex-wrap: wrap; } .markerglowvalue hp-color-opacity { flex: none; } .markerlightdisabled { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2) !important; } .markerlightdisabled ha-icon { --mdc-icon-size: 18px; flex: none; } .markerbadgetechnical { min-width: 0; margin: var(--sp-1) 0 var(--sp-2) !important; overflow-wrap: anywhere; } .markerbadgetechnical code { white-space: normal; } .planrow { display: flex; align-items: center; gap: 10px; } /* the "already uploaded" picker: a plan is never deleted for being unreferenced, so it has to be findable again */ .savedplans { display: flex; flex-direction: column; gap: var(--sp-3); max-height: 240px; overflow: auto; margin: var(--sp-3) 0 var(--sp-1); padding: var(--sp-3); border: 1px solid var(--hp-line); border-radius: var(--rad-m); background: var(--hp-bg2, rgba(255, 255, 255, 0.03)); /* The same collapse that ate .candlist (v1.53.1): a scroll box is a flex item whose automatic minimum size is ZERO (overflow != visible), so inside hp-dialog .body — a flex column taller than its 66vh cap — it shrank to a 14px sliver: the rows were in the DOM, the owner saw a thin rounded stripe under the "Already uploaded" button. Don't shrink, and keep a floor even when the box is empty or loading. */ flex: 0 0 auto; min-height: 2.6em; } .savedplan { display: flex; align-items: center; gap: 10px; } .savedplan.cur { outline: 1px solid var(--hp-accent); border-radius: var(--rad-s); } .savedplan img { width: 56px; height: 40px; object-fit: contain; border: 1px solid var(--hp-line); border-radius: var(--rad-s); background: #fff; flex: none; } .savedmeta { display: flex; flex-direction: column; min-width: 0; flex: 1; } .savedmeta b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .savedmeta .muted { font-size: var(--fs-s); } .savedplan .btn.danger ha-icon { color: #f25a4a; } .savedplan .btn[disabled] { opacity: 0.4; pointer-events: none; } .planprev { max-width: 120px; max-height: 70px; border: 1px solid var(--hp-line); border-radius: var(--rad-s); background: #fff; } .planname { font-size: var(--fs-m); max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .planname.muted { color: var(--hp-muted); } .filebtn { cursor: pointer; } .fileupload { display: inline-flex; min-width: 0; } .fileupload > input { display: none; } .btn.danger { border-color: #b3402a; color: #ff7a5c; } hp-dialog .row .spacer { flex: 1; } hp-dialog .body { max-height: 66vh; overflow-y: auto; } .descin { width: 100%; box-sizing: border-box; background: var(--hp-bg); border: 1px solid var(--hp-line); color: var(--hp-txt); border-radius: var(--rad-s); padding: var(--sp-3) var(--sp-4); font-size: var(--fs-m); font-family: inherit; resize: vertical; /* flex column of the dialog body squeezes textareas — keep a usable height */ min-height: 92px; flex-shrink: 0; line-height: 1.35; } .bindsel { display: flex; flex-direction: column; gap: var(--sp-3); border: 1px solid var(--hp-line); border-radius: var(--rad-m); padding: var(--sp-4); } .bindsel .opt { display: flex; align-items: center; gap: var(--sp-3); border: 1px solid var(--hp-line); background: transparent; color: var(--hp-txt); border-radius: var(--rad-s); padding: var(--sp-3) var(--sp-4); cursor: pointer; font-size: var(--fs-m); font-family: inherit; } .bindsel .opt.on { background: var(--hp-accent); color: var(--text-primary-color, #fff); border-color: var(--hp-accent); } .curbind { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-m); color: var(--hp-txt); flex-wrap: wrap; } .curbind .ref { color: var(--hp-muted); font-size: var(--fs-s); } /* live vacuum: a round puck, no badge plate, soft pulse (docs/VACUUM.md) */ .vacpuck { position: absolute; /* the base badge, but round and 20% smaller — the owner's wording: «иконка похожа на иконку базы, только круглая и чуть меньше» */ --puck-size: calc(var(--device-base-size, 2.25cqw) * 0.8); width: var(--puck-size); height: var(--puck-size); border-radius: 50%; transform: translate(-50%, -50%); --vac-core-bg: light-dark(#fff, #252525); --vac-core-fg: light-dark(#252525, #fff); background: var(--vac-core-bg); border: 1px solid #BCBCBC; box-shadow: 0 1px 2px rgb(37 40 45 / 12%), 0 4px 8px -1.07px rgb(37 40 45 / 18%); color: var(--vac-core-fg); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 6; /* glide between sparse position updates; .jump disables it so the robot never appears to drive through walls after a data gap */ transition: left 1.2s linear, top 1.2s linear; animation: vacpulse 2.2s ease-out infinite; } .vacpuck.theme-light { --vac-core-bg: #fff; --vac-core-fg: #252525; } .vacpuck.theme-dark { --vac-core-bg: #252525; --vac-core-fg: #fff; } .vacpuck.jump { transition: none; } .vacpuck.stale { opacity: 0.45; animation: none; } .vacpuck ha-icon { --mdc-icon-size: calc(var(--puck-size) * 0.68); color: var(--vac-core-fg); /* same centering recipe as .dev ha-icon: without flex + line-height 0 the glyph sits on its text baseline and appears to float around the circle (owner report 2026-07-31) */ display: flex; align-items: center; justify-content: center; line-height: 0; width: var(--mdc-icon-size); height: var(--mdc-icon-size); } @keyframes vacpulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hp-accent) 45%, transparent); } 70% { box-shadow: 0 0 0 12px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } } @media (prefers-reduced-motion: reduce) { .vacpuck { animation: none; } } .vactrail { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 5; overflow: visible; } .vactrail polyline, .vactrail path { fill: none; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; } /* dark halo + light core: neutral, and one of the two always contrasts with whatever fill is underneath */ .vactrail g.prev { opacity: 0.4; } .vactrail .case { stroke: rgba(0, 0, 0, 0.4); stroke-width: 2.25; } .vactrail .core { stroke: rgba(255, 255, 255, 0.82); stroke-width: 0.9; } .vacbox .vacbtns { display: flex; gap: var(--sp-4); margin: var(--sp-3) 0; flex-wrap: wrap; } .vacdiag { display: grid; gap: 4px; margin-bottom: var(--sp-3); } .vacdiag > div { display: flex; justify-content: space-between; gap: var(--sp-5); } .vacdiag > div > span { color: var(--secondary-text-color); } .vacdiag > div > b { text-align: right; overflow-wrap: anywhere; } .vacpicker { margin: var(--sp-3) 0; } .vacsource-warning { display: grid; gap: 8px; } .vacsource-warning .btn { justify-self: start; } .vacpicker > summary { display: inline-flex; width: auto; list-style: none; cursor: pointer; } .vacpicker > summary::-webkit-details-marker { display: none; } .vacsource-list { display: grid; gap: 6px; margin-top: 8px; } .vacsource-list details { padding: 6px 0 0; } .vacsource-list details > summary { cursor: pointer; font-weight: 600; } .vacsource { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; min-width: 0; padding: 9px 10px; border: 1px solid var(--divider-color); border-radius: 10px; color: var(--primary-text-color); background: var(--secondary-background-color); text-align: left; cursor: pointer; } .vacsource.on { border-color: var(--accent-color); box-shadow: inset 3px 0 var(--accent-color); } .vacsource > span:first-child { min-width: 0; display: grid; gap: 2px; } .vacsource small { color: var(--secondary-text-color); overflow-wrap: anywhere; } .vacsource-meta { color: var(--secondary-text-color); text-align: right; font-size: 0.82em; } .vacxcme pre { margin: 8px 0 0; white-space: pre-wrap; user-select: text; } .vacfit { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 12; overflow: visible; touch-action: none; cursor: grab; /* the devlayer is pointer-events: none and every child opts back in — without this line real clicks flew straight through the overlay (owner: «уголки не кликабельны»; synthetic smoke events bypass hit-testing, which is why they lied) */ pointer-events: auto; } .vacfit:active { cursor: grabbing; } .vacfit polygon { fill: color-mix(in srgb, var(--hp-accent) 16%, transparent); stroke: var(--hp-accent); stroke-width: 2; vector-effect: non-scaling-stroke; stroke-dasharray: 6 4; } .vacfit text { fill: var(--hp-accent); font-size: 26px; text-anchor: middle; dominant-baseline: middle; pointer-events: none; user-select: none; } /* room resize tool (docs/RESIZE.md) */ /* wall handle: invisible finger-sized hit circle (HP-1550-04 hit priority kept), the visible glyph lives in the sibling .rszicon */ .rszhandle { fill: transparent; stroke: none; pointer-events: all; cursor: grab; touch-action: none; } .rszhandle:active { cursor: grabbing; } /* wall-with-arrows glyph: accent ink over a bg halo, readable on any plan */ .rszicon { pointer-events: none; } .rszicon path { fill: none; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; } .rszhalo { stroke: var(--hp-bg); stroke-width: 6; } .rszink { stroke: var(--hp-accent); stroke-width: 2; } :host([data-pointer-hover]) .rszhandle:hover + .rszicon .rszink { stroke-width: 3; } /* corner (scale-frame) handles: hit circle invisible, .rszknob is the bead */ .rszcorner { fill: transparent; stroke: none; cursor: nwse-resize; } .rszknob { fill: var(--hp-bg); stroke: var(--hp-accent); stroke-width: 2; vector-effect: non-scaling-stroke; pointer-events: none; } :host([data-pointer-hover]) .rszcorner:hover + .rszknob { fill: var(--hp-accent); } .rszcorner:active { cursor: nwse-resize; } .rszframe { fill: none; stroke: var(--hp-accent); stroke-width: 1.5; stroke-dasharray: 6 5; vector-effect: non-scaling-stroke; pointer-events: none; } /* the decor draft badge rides the MIDDLE of the shape, so it is centred horizontally and lifted clear of the line instead of trailing the cursor the way a wall badge does (owner 2026-08-04) */ .measurelabel.dmeasure { transform: translate(-50%, -160%); } .measurelabel.rszarea { transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.6); } /* width and depth of a piece of furniture while its corner is dragged — centred on the edge they measure (docs/FURNITURE.md §6) */ .measurelabel.furnmeasure { transform: translate(-50%, -50%); border: 1px solid var(--hp-accent); } /* ---- the furniture palette (docs/FURNITURE.md §3) ------------------- */ .furnpalette { display: flex; flex-direction: column; max-height: 38vh; border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); background: var(--card-background-color, var(--hp-bg)); font-size: 0.85em; } .furnhd { display: flex; align-items: center; gap: 6px; padding: 4px 8px; font-weight: 600; opacity: 0.9; } .furnhd .spacer { flex: 1; } .furnbody { overflow: auto; padding: 0 8px 6px; } .furngroup { margin: 6px 0 3px; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.6; } .furnrow { display: flex; flex-wrap: wrap; gap: 4px; } .furnitem { display: flex; flex-direction: column; align-items: center; gap: 2px; width: 74px; padding: 4px 2px; border: 1px solid transparent; border-radius: 8px; background: rgba(127, 127, 127, 0.08); color: inherit; font: inherit; font-size: 0.8em; line-height: 1.15; text-align: center; cursor: pointer; } :host([data-pointer-hover]) .furnitem:hover { background: rgba(127, 127, 127, 0.18); } .furnitem.on { border-color: var(--hp-accent); background: rgba(38, 166, 154, 0.18); } .furnprev { width: 40px; height: 40px; color: var(--primary-text-color, currentColor); } .furnsize { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 6px 8px; border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12)); } .furnsize label { display: flex; align-items: baseline; gap: 3px; opacity: 0.8; } .furnsize .furnunit { opacity: 0.6; font-size: 0.85em; } .furnsize input { width: 5.5em; padding: 3px 5px; } .furnhint { opacity: 0.6; } .vacfitdot { fill: var(--hp-accent); pointer-events: none; } /* hit target: invisible and finger-sized; .vacfitknob is the visible bead */ .vacfithandle { fill: transparent; stroke: none; cursor: nwse-resize; } .vacfitknob { fill: var(--hp-bg); stroke: var(--hp-accent); stroke-width: 2; vector-effect: non-scaling-stroke; pointer-events: none; } .vaccalbar { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); display: flex; gap: var(--sp-5); align-items: center; background: var(--hp-bg); color: var(--hp-txt); border: 1px solid var(--hp-accent); border-radius: var(--rad-l); padding: 10px var(--sp-5); z-index: 60; box-shadow: var(--shadow-2); } .candlist { max-height: 160px; overflow-y: auto; border-top: 1px solid var(--hp-line); /* A scrollable box is a flex item that HAPPILY collapses: inside the dialog body (a flex column) this list rendered its rows into a 1px sliver — the DOM had 26 candidates and the user saw nothing. In the binding dropdown it sits inside .droppanel (block context) and never showed the bug. Field report, 2026-07-30. */ flex: 0 0 auto; min-height: 2.6em; } .cand { display: flex; justify-content: space-between; gap: var(--sp-4); padding: var(--sp-3) var(--sp-4); cursor: pointer; border-radius: var(--rad-s); font-size: var(--fs-m); } :host([data-pointer-hover]) .cand:hover { background: rgba(127, 127, 127, 0.15); } .cand.sel { background: var(--hp-accent); color: var(--text-primary-color, #fff); } .cand .cs { color: var(--hp-muted); font-size: var(--fs-s); white-space: nowrap; } .cand.sel .cs { color: var(--text-primary-color, #fff); opacity: 0.85; } .cand.muted { color: var(--hp-muted); cursor: default; } .pdfedit { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; } .pdftag { display: inline-flex; align-items: center; gap: var(--sp-2); border: 1px solid var(--hp-line); border-radius: var(--rad-s); padding: var(--sp-2) var(--sp-3); font-size: var(--fs-s); } .pdftag a { color: var(--hp-txt); text-decoration: none; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .pdftag .x { --mdc-icon-size: 15px; cursor: pointer; color: var(--hp-muted); } :host([data-pointer-hover]) .pdftag .x:hover { color: #ff7a5c; } .entlist { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: 10px; } .entrow { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-3) var(--sp-4); border-radius: var(--rad-m); background: var(--secondary-background-color, rgba(128, 128, 128, 0.12)); } .entrow ha-icon { --mdc-icon-size: 20px; color: var(--hp-muted); } .entrow.on ha-icon { color: var(--hp-accent); } .entrow .en { flex: 1; font-size: var(--fs-m); } .entrow .ev { font-size: var(--fs-m); color: var(--hp-muted); } .entbtn { min-width: 74px; min-height: 32px; padding: var(--sp-2) var(--sp-5); border: 1px solid var(--hp-muted); border-radius: 999px; background: transparent; color: var(--hp-txt); font: inherit; font-size: var(--fs-m); cursor: pointer; } .entbtn.on { background: var(--hp-accent); border-color: var(--hp-accent); color: var(--text-primary-color, #fff); font-weight: 600; } .inforow { display: flex; gap: 10px; font-size: var(--fs-m); margin: var(--sp-2) 0; } .inforow .k { color: var(--hp-muted); min-width: 84px; } .inforow a { color: var(--hp-accent); word-break: break-all; } .infodesc { font-size: var(--fs-m); white-space: pre-wrap; margin-top: var(--sp-3); } .infodesc.muted { color: var(--hp-muted); } .pdflist { display: flex; flex-direction: column; gap: var(--sp-2); } .pdf { display: inline-flex; align-items: center; gap: var(--sp-2); color: var(--hp-accent); text-decoration: none; } ha-icon-picker { display: block; } .floorrow { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-3) var(--sp-2); font-size: var(--fs-m); cursor: pointer; } .floorrow .floorlvl { color: var(--hp-muted); font-size: var(--fs-s); border: 1px solid var(--hp-line); border-radius: var(--rad-s); padding: 0 var(--sp-3); } .rhint { font-size: var(--fs-s); color: var(--hp-muted); margin-bottom: var(--sp-3); } .togglehint { overflow-wrap: anywhere; } .togglehint > div + div { margin-top: var(--sp-1); } .sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; } .rtest { display: flex; align-items: center; gap: var(--sp-4); margin-bottom: var(--sp-4); } .rtest .namein { flex: 1; } .rtest ha-icon { color: var(--hp-accent); } .rtesticon { font-size: var(--fs-s); color: var(--hp-muted); } .rrow { display: flex; align-items: center; gap: var(--sp-3); margin: var(--sp-1) 0; } .rrow .rpat { flex: 2; } .rrow .ricon { flex: 1.4; } .rrow .rpat.bad { border-color: #ff7a5c; } .rrow .rprev { --mdc-icon-size: 18px; color: var(--hp-txt); min-width: 18px; } .rrow .ract { --mdc-icon-size: 16px; color: var(--hp-muted); cursor: pointer; } :host([data-pointer-hover]) .rrow .ract:hover { color: var(--hp-txt); } :host([data-pointer-hover]) .rrow .ract.del:hover { color: #ff7a5c; } .gsrow .gsl { min-width: 150px; font-size: var(--fs-m); color: var(--hp-muted); } .alignmsg { margin: 0 0 8px; font-size: 13px; line-height: 1.45; } .btn.alignall { width: 100%; justify-content: center; } .backupactions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); } .backupactions .btn { justify-content: center; min-width: 0; } .backupupload { display: inline-flex; min-width: 0; } .backupupload > .btn { width: 100%; justify-content: center; } .backupupload input { display: none; } .backupbody { min-width: 0; } .backupplanonly { margin-inline-start: var(--sp-4) !important; align-items: flex-start !important; } .backupplanonly > span:first-of-type { display: grid; gap: 2px; white-space: normal; } .backupplanonly small { color: var(--secondary-text-color); line-height: 1.35; } .backupplanonlystatus { color: var(--hp-accent) !important; font-weight: 700; } .backupfile, .backupsummary, .backupcontent { display: flex; flex-direction: column; min-width: 0; gap: var(--sp-1); } .backupfile b, .backupcontent span { overflow-wrap: anywhere; } .backupfile span, .backupsummary span, .backupcontent span { color: var(--hp-muted); font-size: var(--fs-s); } .backupcounts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2) var(--sp-4); font-size: var(--fs-s); } .backupwarn, .backuperror { border-radius: var(--rad-s); padding: var(--sp-3); font-size: var(--fs-s); line-height: 1.45; overflow-wrap: anywhere; } .backupwarn { background: color-mix(in srgb, var(--hp-accent) 12%, transparent); } .backuperror { background: rgba(179, 64, 42, .16); color: #ff7a5c; } .backupchoices { display: flex; flex-direction: column; gap: var(--sp-2); border: 1px solid var(--hp-line); border-radius: var(--rad-s); min-width: 0; } .backupchoices label { margin: 0 !important; display: flex; gap: var(--sp-2); } .backupconfirm { align-items: flex-start !important; } .backupconfirm > span:first-of-type { min-width: 0; white-space: normal; overflow-wrap: anywhere; } @media (max-width: 520px) { .backupactions, .backupcounts { grid-template-columns: 1fr; } } .aboutver { font-size: var(--fs-s); color: var(--hp-muted); margin: var(--sp-2) 0 var(--sp-3); } .aboutlink { display: flex; align-items: center; gap: var(--sp-3); width: fit-content; color: var(--hp-accent); text-decoration: none; font-size: var(--fs-m); padding: var(--sp-1) 0; } :host([data-pointer-hover]) .aboutlink:hover { text-decoration: underline; } .aboutlink ha-icon { --mdc-icon-size: 18px; line-height: 1; } hp-dialog .body { padding: var(--sp-5) var(--sp-6); display: flex; flex-direction: column; gap: var(--sp-3); } hp-dialog .tapconfirm-body { min-width: 0; overflow-x: hidden; } hp-dialog .tapconfirm-body p { max-width: 100%; min-width: 0; margin: 0; overflow-wrap: anywhere; white-space: normal; } hp-dialog .tapconfirm-line { color: var(--hp-muted); } hp-dialog .body label { font-size: var(--fs-s); color: var(--hp-muted); margin-top: var(--sp-3); } hp-dialog .body .namein, hp-dialog .body .areasel { width: 100%; box-sizing: border-box; } /* Room settings contains long radio labels, two source pickers and live previews, so it deliberately uses hp-dialog's medium width. Keep its content shrinkable as well: the generic srcrow rule is nowrap because compact switch rows need it, but here that created a horizontal scroll box even on a wide desktop viewport. */ hp-dialog.roomdialog .body { min-width: 0; overflow-x: hidden; } hp-dialog.roomdialog .body > * { min-width: 0; max-width: 100%; box-sizing: border-box; } hp-dialog.roomdialog .srcrow { min-width: 0; align-items: flex-start; } hp-dialog.roomdialog .srcrow > span:first-of-type { min-width: 0; white-space: normal; overflow-wrap: anywhere; line-height: 1.35; } hp-dialog.roomdialog .dropbtn > b, hp-dialog.roomdialog .dropbtn > .ref { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .devicepreview-empty { min-height: 82px; margin: 4px 0 14px; padding: var(--sp-4); display: flex; align-items: center; justify-content: center; gap: var(--sp-3); border: 1px dashed var(--hp-line); border-radius: var(--rad-m); color: var(--hp-muted); text-align: center; } hp-dialog .row { display: flex; justify-content: flex-end; gap: var(--sp-4); width: 100%; min-width: 0; box-sizing: border-box; padding: var(--sp-5) var(--sp-6); border-top: 1px solid var(--hp-line); } /* Stable destructive/commit footer contract. A flex spacer cannot react when translated labels no longer fit: justify-content then overflows the destructive button through the left inset. Two real groups wrap as units instead — destructive actions stay left, while Cancel/Save move together to a right-aligned second row when necessary. */ hp-dialog .row.dialog-action-footer { align-items: center; justify-content: flex-start; flex-wrap: wrap; row-gap: var(--sp-4); } hp-dialog .dialog-action-group { display: flex; flex: 0 1 auto; flex-wrap: wrap; align-items: center; gap: var(--sp-4); max-width: 100%; min-width: 0; } hp-dialog .dialog-action-group .btn { flex: 0 0 auto; min-height: 44px; } hp-dialog .dialog-action-danger { margin-right: auto; } hp-dialog .dialog-action-commit { margin-left: auto; justify-content: flex-end; } hp-dialog .row.markerfooter { justify-content: space-between; align-items: center; flex-wrap: wrap; } hp-dialog .row.roomfooter { align-items: center; flex-wrap: wrap; } /* Keep the last editor bar mounted while the row collapses. This makes both entering and leaving an editor change the card geometry gradually; the header ResizeObserver keeps the stage fitted throughout. */ .editorchrome { display: block; height: 0; opacity: 0; visibility: hidden; overflow: hidden; } .editorchrome:not(.open) { pointer-events: none; } .editorchrome.open { height: auto; opacity: 1; visibility: visible; overflow: visible; } .editorchrome.transitioning { overflow: hidden; will-change: height; pointer-events: none; } /* The toolbar is already visible while its height is interpolating. Its explicit navigation control must remain usable even though all editing tools stay frozen until the transition settles. */ .editorchrome.transitioning .barclose { pointer-events: auto; } .editorchrome-inner { min-height: 0; transform-origin: top center; } .editorchrome.transitioning .editorchrome-inner { will-change: opacity; } @media (prefers-reduced-motion: reduce) { .modetab { transition: none; } } /* Device info can have Edit + Open in HA + Close. It uses a wide dialog; wrapping remains as a phone fallback, but without a flex spacer (which used to strand Edit alone on the first line). */ hp-dialog .row.infofooter { align-items: center; justify-content: flex-start; flex-wrap: wrap; gap: var(--sp-3); } hp-dialog .row.infofooter .btn { flex-shrink: 0; } hp-dialog .row.infofooter .infofooter-close { margin-left: auto; } @media (max-width: 480px) { hp-dialog .row.infofooter { padding: var(--sp-4) var(--sp-5); } } .markeractions, .markersaveactions { display: flex; align-items: center; gap: var(--sp-4); } .markeractions:empty { display: none; } .markersaveactions { margin-left: auto; } .editbar .warn { color: #ffc14d; } .editbar .sname { font-weight: 600; } .editbar input { width: 74px; background: transparent; border: 1px solid var(--hp-line); color: var(--hp-txt); border-radius: var(--rad-s); padding: var(--sp-3) var(--sp-4); font-size: var(--fs-m); } .editbar label, .editbar .hint { color: var(--hp-muted); font-size: var(--fs-s); } .editbar .drawwall { display: inline-flex; align-items: center; gap: var(--sp-3); white-space: nowrap; } .editbar .wallsgroup { display: inline-flex; align-items: center; gap: 10px; white-space: nowrap; } .editbar .drawwall input { width: 4.2em; } .editbar .drawwall .opl { color: var(--hp-muted); font-size: var(--fs-s); } .menuwrap { position: fixed; inset: 0; z-index: 80; } .menu { position: fixed; background: var(--hp-bg); border: 1px solid var(--hp-accent); border-radius: var(--rad-l); box-shadow: var(--shadow-2); min-width: 210px; max-width: 300px; overflow: hidden; transform: translate(0, 8px); } .menu .hd { padding: var(--sp-4) var(--sp-5); font-weight: 600; font-size: var(--fs-m); border-bottom: 1px solid var(--hp-line); display: flex; align-items: center; gap: var(--sp-3); } .menu .hd ha-icon, .menu .it.all ha-icon { color: var(--hp-accent); --mdc-icon-size: 16px; } .menu .it { padding: var(--sp-4) var(--sp-5); font-size: var(--fs-m); cursor: pointer; display: flex; align-items: center; gap: var(--sp-4); } .menu .it ha-icon { --mdc-icon-size: 16px; color: var(--hp-muted); } :host([data-pointer-hover]) .menu .it:hover { background: rgba(127, 127, 127, 0.15); } .menu .it.all { color: var(--hp-accent); font-weight: 600; } .tip { position: fixed; pointer-events: none; background: var(--hp-bg); border: 1px solid var(--hp-accent); color: var(--hp-txt); padding: var(--sp-3) 10px; border-radius: var(--rad-m); font-size: var(--fs-m); box-shadow: var(--shadow-2); z-index: 60; max-width: 260px; } .tip .m { color: var(--hp-muted); font-size: var(--fs-s); display: block; } .toast { position: fixed; pointer-events: none; left: 50%; bottom: 22px; transform: translateX(-50%); background: var(--hp-bg); border: 1px solid var(--hp-accent); color: var(--hp-txt); padding: var(--sp-4) var(--sp-6); border-radius: var(--rad-l); font-size: var(--fs-m); box-shadow: var(--shadow-2); z-index: 120; max-width: 90vw; } `;