mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
3691 lines
117 KiB
TypeScript
3691 lines
117 KiB
TypeScript
/** 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.drop-before { box-shadow: inset 2px 0 0 0 var(--primary-color, #03a9f4); }
|
||
.tab.drop-after { 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 <line>,
|
||
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 *,
|
||
.hidden-wall-diagnostic,
|
||
.hidden-wall-diagnostic * {
|
||
pointer-events: none;
|
||
}
|
||
.plan-snap-line,
|
||
.hidden-wall-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,
|
||
.hidden-wall-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%);
|
||
}
|
||
.hidden-wall-line {
|
||
stroke: color-mix(in srgb, var(--hp-accent) 72%, white 28%);
|
||
}
|
||
.plan-snap-node,
|
||
.hidden-wall-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,
|
||
.hidden-wall-line {
|
||
stroke: CanvasText;
|
||
opacity: 1;
|
||
forced-color-adjust: auto;
|
||
}
|
||
.plan-snap-node,
|
||
.hidden-wall-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; }
|
||
.rszhandle.disabled,
|
||
.rszhandle.disabled:active {
|
||
cursor: not-allowed;
|
||
}
|
||
/* 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; }
|
||
.rszicon.disabled { opacity: 0.38; }
|
||
:host([data-pointer-hover]) .rszhandle.disabled:hover + .rszicon .rszink { stroke-width: 2; }
|
||
.rszmeasurelayer,
|
||
.rszmeasurelayer * { pointer-events: none; }
|
||
.rszmeasurehalo,
|
||
.rszmeasureink,
|
||
.rszleader {
|
||
fill: none;
|
||
stroke-linecap: round;
|
||
vector-effect: non-scaling-stroke;
|
||
}
|
||
.rszmeasurehalo {
|
||
stroke: var(--hp-bg);
|
||
stroke-width: 7px;
|
||
opacity: 0.9;
|
||
}
|
||
.rszmeasureink {
|
||
stroke: var(--hp-accent);
|
||
stroke-width: 3px;
|
||
}
|
||
.rszleader {
|
||
stroke: var(--hp-accent);
|
||
stroke-width: 2px;
|
||
opacity: 0.95;
|
||
}
|
||
/* 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(
|
||
calc(-50% + var(--rsz-label-x, 0px)),
|
||
calc(-50% + var(--rsz-label-y, 0px))
|
||
);
|
||
background: rgba(0, 0, 0, 0.6);
|
||
border: 1px solid var(--hp-accent);
|
||
}
|
||
/* 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;
|
||
}
|
||
.opening-entity-candidate {
|
||
width: 100%;
|
||
border: 0;
|
||
background: transparent;
|
||
color: inherit;
|
||
font: inherit;
|
||
text-align: left;
|
||
}
|
||
.opening-entity-candidate.sel {
|
||
background: var(--hp-accent);
|
||
color: var(--text-primary-color, #fff);
|
||
}
|
||
.opening-entity-empty { cursor: default; }
|
||
.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; }
|
||
.optimize-live {
|
||
display: grid;
|
||
justify-items: start;
|
||
gap: var(--sp-2);
|
||
margin-bottom: var(--sp-3);
|
||
}
|
||
.optimize-live .alignmsg, .optimize-live .rhint { margin-bottom: 0; }
|
||
.optimize-cleanup { min-height: 44px; }
|
||
.optimize-selected { color: var(--hp-txt); }
|
||
.optimize-details {
|
||
margin-top: var(--sp-3);
|
||
color: var(--hp-muted);
|
||
font-size: var(--fs-s);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.optimize-details > summary {
|
||
width: fit-content;
|
||
color: var(--hp-txt);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
}
|
||
.optimize-details > summary:focus-visible {
|
||
outline: 2px solid var(--hp-accent);
|
||
outline-offset: 3px;
|
||
border-radius: var(--rad-s);
|
||
}
|
||
.optimize-details ul { margin: var(--sp-3) 0; padding-inline-start: 22px; }
|
||
.optimize-details li + li { margin-top: var(--sp-1); }
|
||
.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; }
|
||
.backupdetails {
|
||
border: 1px solid var(--hp-line);
|
||
border-radius: var(--rad-s);
|
||
padding: var(--sp-2) var(--sp-3);
|
||
font-size: var(--fs-s);
|
||
}
|
||
.backupdetails summary { cursor: pointer; font-weight: 700; }
|
||
.backupdetails > div { display: grid; gap: var(--sp-1); padding-block-start: var(--sp-2); }
|
||
.backupdetails code { overflow-wrap: anywhere; color: var(--hp-muted); }
|
||
.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;
|
||
}
|
||
`;
|