/** 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); } .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; } .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; } .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)); } /* A deliberate navigation change gets one short visual settle. The class is transient, so normal day/night breathing keeps its slow cadence. */ .stage.hpnav, .stage.daynight.hpnav { transition: background-color 0.18s ease, outline-color 0.18s ease; } .zoomwrap { position: absolute; inset: 0; } /* Sun on the plan (docs/SUN.md): the stage breathes with the day over tens of seconds; the plan itself dims at most ~10%. Reduced motion = static. */ .stage.daynight { transition: background-color 45s linear; } .stage.daynight .zoomwrap { transition: filter 45s linear; } .stage.daynight.hpsettle { transition: height 0.25s ease, background-color 45s linear; } /* Catch-up frame (docs/SUN.md): the sky is out of date because the card was not painting — show the truth at once, the glide comes back on the very next frame. */ .stage.daynight.skysnap, .stage.daynight.skysnap .zoomwrap, .stage.daynight.skysnap.hpsettle { transition: none; } @media (prefers-reduced-motion: reduce) { .stage.daynight, .stage.daynight .zoomwrap, .stage.daynight.hpsettle { 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; } /* A long-suspended normal View wakes through transient HA/viewport geometry. Keep those few frames behind the already-correct stage background; the final viewport and reveal are committed together. This class is never set in kiosk or an editor. */ .stage.hpresume .zoomwrap, .stage.hpresume .zoombadge, .stage.hpresume .farhint, .stage.hpresume .homearrow { 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; } @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; } .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; } /* 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; 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; } /* 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; } /* White day (owner 2026-08-03): at high sun the daynight sky is #ffffff — the same white as a drawn plan's paper. A whisper of a drop shadow on the paper GROUP keeps the sheet's contour readable on the white sky (user units on the 1000-unit canvas ≈ a few screen px). Invisible at night against the dark sky; 'static' mode never gets it. */ .stage.daynight .hp-paperg { filter: drop-shadow(0 2px 8px rgba(10, 16, 26, 0.28)); } /* 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: 2; } .stage.mode-view .room.overlay:not(.styled):hover { fill: var(--hp-accent); fill-opacity: 0.18; stroke: var(--hp-accent); } .room.yard { fill: rgba(75, 140, 90, 0.14); stroke: #4b8c5a; stroke-width: 2; } .stage.mode-view .room.yard:not(.styled):hover { fill: var(--hp-accent); fill-opacity: 0.2; stroke: var(--hp-accent); } .room.styled { stroke: var(--room-stroke, transparent); stroke-opacity: var(--room-stroke-op, 0); stroke-width: 2.5; fill: var(--room-fill, transparent); fill-opacity: var(--room-fill-op, 0); } .glow-base-layer, .glow-base-tunnels, .glow-pools-frame, .glow-pools { pointer-events: none; } /* The isolated group makes source pools blend with one another, never with the room data fill, Glow base, paper or backdrop. Outer opacity is the existing 0.7 and is applied once after this flat group is composited. */ .glow-pools-frame, .glow-pools { isolation: isolate; } .glow-pools.blend-screen .glow-pool { mix-blend-mode: screen; } /* View hover: brighten the current fill; accent wash when unfilled. */ .stage.mode-view .room.styled.filled:hover { filter: brightness(1.2) saturate(1.08) drop-shadow(0 0 4px var(--hp-accent)); stroke: var(--hp-accent); stroke-opacity: 1; } .stage.mode-view .room.styled:not(.filled):hover { fill: var(--hp-accent); fill-opacity: 0.18; stroke-opacity: 1; } /* doors, windows & gates */ .op-leaf { transition: transform 0.6s ease; } .op-arc { stroke-width: 1.5; 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: 1.5; opacity: 0; transition: opacity 0.15s; pointer-events: none; } .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; } /* 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 { pointer-events: none; /* inert while editing; clickable in View (rule below) */ position: absolute; transform: translate(-50%, -50%); width: calc(var(--icon-size, 2.5cqw) * 0.62); height: calc(var(--icon-size, 2.5cqw) * 0.62); border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--hp-bg); border: 1px solid var(--hp-line); z-index: 1; } .stage.mode-view .oplock { pointer-events: auto; cursor: pointer; } .oplock ha-icon { --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.4); display: flex; line-height: 0; } .oplock.locked { color: #66d17a; border-color: #66d17a; } .oplock.unlocked { color: var(--hp-open); border-color: var(--hp-open); } .oplock.unknown { color: 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; } } /* Semantic activity: event / presence / transition / actual running all share one ring layer and one colour/size override. */ .dev ha-icon { position: relative; z-index: 1; } .activity-ring { position: absolute; left: 50%; top: 50%; width: calc(var(--dev-size) * var(--ripple-scale, 3)); height: calc(var(--dev-size) * var(--ripple-scale, 3)); transform: translate(-50%, -50%); pointer-events: none; z-index: 0; } .activity-ring 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. */ .activity-ring.event i { animation: hp-activity-event 1.1s ease-out 1 forwards; } .activity-ring.event i:nth-child(2) { animation-delay: 1.1s; } .activity-ring.event i:nth-child(3) { animation-delay: 2.2s; } .activity-ring.event.gen2 i { animation-name: hp-activity-event-b; } /* Presence is a state, not an event: calm and deliberately static. */ .activity-ring.presence i:first-child { opacity: 0.4; } .activity-ring.presence i:nth-child(n + 2), .activity-ring.transition i:nth-child(n + 2), .activity-ring.running i:nth-child(n + 2) { display: none; } /* Physical travel and actual work use related but distinct tempos. */ .activity-ring.transition i:first-child { animation: hp-activity-breathe 2.2s ease-in-out infinite; } .activity-ring.running i:first-child { animation: hp-activity-running 2.8s ease-in-out infinite; } @keyframes hp-activity-event { 0% { transform: scale(0.18); opacity: 0.7; } 70% { opacity: 0.22; } 100% { transform: scale(1); opacity: 0; } } /* Alternate identity: a rapid retrigger restarts the browser timeline. */ @keyframes hp-activity-event-b { 0% { transform: scale(0.18); opacity: 0.7; } 70% { opacity: 0.22; } 100% { transform: scale(1); opacity: 0; } } @keyframes hp-activity-breathe { 0% { transform: scale(0.92); opacity: 0.16; } 50% { transform: scale(1.04); opacity: 0.5; } 100% { transform: scale(0.92); opacity: 0.16; } } @keyframes hp-activity-running { 0% { transform: scale(0.94); opacity: 0.18; } 50% { transform: scale(1.02); opacity: 0.42; } 100% { transform: scale(0.94); opacity: 0.18; } } @media (prefers-reduced-motion: reduce) { .activity-ring i { animation: none !important; } .activity-ring i:first-child { opacity: 0.4; } .activity-ring i:nth-child(n + 2) { display: none; } } .roomlabel { pointer-events: none; /* draggable only in plan mode (rule below) */ position: absolute; transform: translate(-50%, -50%); font-size: calc(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; } } @keyframes hp-editor-enter { 0% { transform: translateY(5px) scale(0.997); opacity: 0.72; } 100% { transform: translateY(0) scale(1); opacity: 1; } } @keyframes hp-editor-exit { 0% { transform: translateY(-4px) scale(1.002); opacity: 0.72; } 100% { transform: translateY(0) scale(1); opacity: 1; } } @keyframes hp-editor-swap { 0% { transform: scale(0.997); opacity: 0.68; } 100% { transform: scale(1); 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); } .zoomwrap.nav-enter { animation: hp-editor-enter 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } .zoomwrap.nav-exit { animation: hp-editor-exit 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } .zoomwrap.nav-swap { animation: hp-editor-swap 0.16s ease-out; } @media (prefers-reduced-motion: reduce) { .stage.hpnav, .stage.daynight.hpnav { transition: none; } .zoomwrap.slide-left, .zoomwrap.slide-right, .zoomwrap.nav-enter, .zoomwrap.nav-exit, .zoomwrap.nav-swap { 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; } .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; } .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; } .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); } .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; } .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: 1.2; stroke-dasharray: 4 4; 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; } /* 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: 6 5; 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; } .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; } .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; } /* decor mode: everything but the decor itself fades back */ .stage.mode-decor .room, .stage.mode-decor .devlayer, .stage.mode-decor .opening, .stage.mode-decor .rlabel, .stage.mode-decor .room-outline, .stage.mode-decor .wallbodies, .stage.mode-decor .opening-tunnels, .stage.mode-decor .glow-base-layer, .stage.mode-decor .glow-pools-frame, .stage.mode-decor .openwalls { opacity: 0.35; } .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; } .opghost { stroke: var(--hp-open, #ff9800); stroke-width: 5; stroke-linecap: round; stroke-dasharray: 7 6; opacity: 0.85; pointer-events: none; } .opghost-dot { fill: var(--hp-open, #ff9800); opacity: 0.85; pointer-events: none; } .rlabel { fill: var(--hp-muted); font-size: 15px; font-weight: 600; pointer-events: none; text-anchor: middle; } .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: 0.6; 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: 2.5; 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: 2.5; stroke-dasharray: 7 7; 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: 2.5; 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; } /* View hover follows the clean-floor face above the unioned wall body. The original room stroke remains responsible for fill and thin-wall fallback. */ .room-hover-outline { fill: none; stroke: var(--hp-accent); stroke-width: 3; stroke-linejoin: round; stroke-linecap: round; pointer-events: none; filter: drop-shadow(0 0 3px var(--hp-accent)); } .openwall-preview { stroke: #ffc14d; stroke-width: 5; stroke-dasharray: 7 7; 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: 2.5; 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; } .modetab .closex { --mdc-icon-size: 13px; display: inline-flex; align-items: center; margin-left: 2px; opacity: 0.75; cursor: pointer; border-radius: var(--rad-s); } .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: 3; 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: 2.5; stroke-linecap: round; stroke-linejoin: round; } .pathline { stroke: #ffc14d; stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; } .preview { stroke: #ffc14d; stroke-width: 2; stroke-dasharray: 6 5; opacity: 0.7; } .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: 0.5; stroke-opacity: 0.7; pointer-events: none; } .vertex { fill: #ffc14d; stroke: #4a2800; stroke-width: 1; } .vertex.first { fill: #4bd28f; stroke: #04121f; } .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 { /* all satellite metrics from --dev-size, not --icon-size: the per-device size multiplier must scale the value plate with its marker — same bug class as the v1.51.3 glyph fix (pinned to base size, the multiplier grew nothing). */ width: auto; min-width: var(--dev-size, var(--icon-size, 2.5cqw)); max-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 4); box-sizing: border-box; padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16); } .dev.valonly .valtext { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.45); font-weight: 700; 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; } } /* alarms pulse red over everything */ .dev.alarm, .dev.alarm:hover { background: #6f2325; border-color: #f25a4a; color: #fff; box-shadow: 0 0 10px rgba(242, 90, 74, 0.65); } .dev.alarm::after { content: ''; position: absolute; inset: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * -0.35); border: 3px solid #f25a4a; border-radius: 50%; animation: hp-alarm 1s ease-out infinite; pointer-events: none; } @keyframes hp-alarm { 0% { transform: scale(0.7); opacity: 1; } 100% { transform: scale(1.25); opacity: 0; } } @media (prefers-reduced-motion: reduce) { .dev.alarm::after { animation: none; opacity: 0.9; } } .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; } .dev { position: absolute; /* per-device multiplier on top of the card-wide icon size */ --dev-size: calc(var(--icon-size, 2.5cqw) * var(--dev-scale, 1)); /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */ width: var(--dev-size); height: var(--dev-size); margin: calc(var(--dev-size) / -2 - 1px) 0 0 calc(var(--dev-size) / -2 - 1px); border-radius: 22%; background: var(--hp-bg); border: 1px solid var(--hp-line); display: flex; align-items: center; justify-content: center; color: var(--hp-txt); cursor: pointer; pointer-events: auto; transition: background 0.15s, border-color 0.15s, opacity 0.2s; box-shadow: var(--shadow-1); z-index: 2; } .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(--icon-size, 2.5cqw)) * 0.62); 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:hover { background: var(--hp-accent); color: var(--text-primary-color, #fff); z-index: 5; } .dev.on { background: var(--hp-on); border-color: var(--hp-on); color: #503c00; box-shadow: 0 0 8px rgba(255, 212, 92, 0.7); } .dev.open { background: var(--hp-open); border-color: var(--hp-open); color: #4a2800; } .dev.unavail { opacity: 0.35; } .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 { 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; border-style: dashed; border-color: var(--hp-accent); 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); box-shadow: none; } /* 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; border-color: var(--secondary-text-color, #9aa0aa); 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 { border-color: #ffc14d; box-shadow: 0 0 0 3px rgba(255, 193, 77, 0.35); } .dev .tval { position: absolute; left: 100%; top: 50%; transform: translateY(-50%); margin-left: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1); background: var(--card-background-color, var(--hp-bg)); border: 1px solid var(--hp-accent); border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.18); padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14); font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.45); font-weight: 700; line-height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.68); color: var(--hp-txt); white-space: nowrap; pointer-events: none; } .dev .hval { position: absolute; left: 100%; top: 50%; transform: translateY(-50%); margin-left: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1); background: var(--card-background-color, var(--hp-bg)); border: 1px solid #4fc3f7; border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.18); padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14); font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.45); font-weight: 700; line-height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.68); color: var(--hp-txt); white-space: nowrap; pointer-events: none; } .dev .lqi { position: absolute; top: 100%; 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: 700; 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; } .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; } .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); } .colorrow input[type='color'] { width: 42px; height: 28px; border: 1px solid var(--hp-line); border-radius: var(--rad-s); background: transparent; padding: 1px; cursor: pointer; } .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; } .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; } .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(--dev-size, var(--icon-size, 2.5cqw)) * 0.8); width: var(--puck-size); height: var(--puck-size); border-radius: 50%; transform: translate(-50%, -50%); background: var(--hp-bg); border: 1px solid var(--hp-line); box-shadow: var(--shadow-1); color: var(--hp-txt); 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.jump { transition: none; } .vacpuck.stale { opacity: 0.45; animation: none; } .vacpuck ha-icon { --mdc-icon-size: calc(var(--puck-size) * 0.68); color: var(--hp-txt); /* 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; } .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; } .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; } .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); } .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); } .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); } .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; } .rrow .ract:hover { color: var(--hp-txt); } .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; } .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; } .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 .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); padding: var(--sp-5) var(--sp-6); border-top: 1px solid var(--hp-line); } 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: grid; grid-template-rows: 0fr; opacity: 0; visibility: hidden; overflow: hidden; transition: grid-template-rows 0.18s cubic-bezier(0.2, 0.7, 0.2, 1), opacity 0.12s ease, visibility 0s linear 0.18s; } .editorchrome:not(.open) { pointer-events: none; } .editorchrome.open { grid-template-rows: 1fr; opacity: 1; visibility: visible; overflow: visible; transition-delay: 0s; } .editorchrome.resizing { overflow: hidden; will-change: height; } .editorchrome.open.nav-enter { overflow: hidden; } .editorchrome-inner { min-height: 0; transform-origin: top center; } .editorchrome-inner.nav-enter { animation: hp-editor-enter 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } .editorchrome-inner.nav-exit { animation: hp-editor-exit 0.18s cubic-bezier(0.2, 0.7, 0.2, 1); } @media (prefers-reduced-motion: reduce) { .modetab { transition: none; } .editorchrome { transition: none; } .editorchrome-inner.nav-enter, .editorchrome-inner.nav-exit { animation: none; 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); } .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; } `;