mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
3042 lines
95 KiB
TypeScript
3042 lines
95 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);
|
|
}
|
|
.hdr {
|
|
position: sticky;
|
|
top: var(--header-height, 56px);
|
|
z-index: 20;
|
|
background: var(--card-background-color, var(--hp-bg));
|
|
border-radius: var(--ha-card-border-radius, 12px) var(--ha-card-border-radius, 12px) 0 0;
|
|
}
|
|
.head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px var(--sp-5);
|
|
border-bottom: 1px solid var(--hp-line);
|
|
flex-wrap: wrap;
|
|
}
|
|
.title {
|
|
font-size: var(--fs-l);
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
white-space: nowrap;
|
|
}
|
|
.title ha-icon {
|
|
color: var(--hp-accent);
|
|
--mdc-icon-size: 18px;
|
|
}
|
|
.tabs {
|
|
display: flex;
|
|
gap: var(--sp-2);
|
|
background: rgba(127, 127, 127, 0.12);
|
|
padding: var(--sp-2);
|
|
border-radius: var(--rad-l);
|
|
flex-wrap: wrap;
|
|
}
|
|
@media (max-width: 620px) {
|
|
.head { gap: var(--sp-3); padding: var(--sp-4) 10px; }
|
|
.head .count { display: none; }
|
|
.head .title { font-size: var(--fs-m); }
|
|
}
|
|
.tab {
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--hp-muted);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: var(--sp-3) var(--sp-5);
|
|
border-radius: var(--rad-m);
|
|
font-size: var(--fs-m);
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: 0.15s;
|
|
font-family: inherit;
|
|
}
|
|
.tab:hover {
|
|
color: var(--hp-txt);
|
|
}
|
|
.tab.active {
|
|
background: var(--hp-accent);
|
|
color: var(--text-primary-color, #fff);
|
|
}
|
|
.count {
|
|
font-size: var(--fs-s);
|
|
color: var(--hp-muted);
|
|
}
|
|
.spacer {
|
|
flex: 1;
|
|
}
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
border: 1px solid var(--hp-line);
|
|
background: transparent;
|
|
color: var(--hp-txt);
|
|
padding: var(--sp-3) 10px;
|
|
border-radius: var(--rad-m);
|
|
cursor: pointer;
|
|
transition: 0.15s;
|
|
font-family: inherit;
|
|
font-size: var(--fs-m);
|
|
}
|
|
.btn ha-icon {
|
|
--mdc-icon-size: 17px;
|
|
}
|
|
.btn:hover {
|
|
border-color: var(--hp-accent);
|
|
}
|
|
.btn.on {
|
|
background: var(--hp-accent);
|
|
color: var(--text-primary-color, #fff);
|
|
border-color: var(--hp-accent);
|
|
}
|
|
.btn.ghost {
|
|
border: none;
|
|
}
|
|
.btn[disabled] {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
.stage.noplan {
|
|
background: #ffffff;
|
|
}
|
|
/* Editors always keep the white drawing sheet — even when a backdrop
|
|
image is loaded. Without this, a picture plan painted the theme card
|
|
colour under the grid (owner 2026-08-05). View keeps the historical
|
|
theme / .noplan split. */
|
|
.stage.mode-plan,
|
|
.stage.mode-devices,
|
|
.stage.mode-decor {
|
|
background: #ffffff;
|
|
}
|
|
.stage {
|
|
position: relative;
|
|
width: 100%;
|
|
container-type: inline-size;
|
|
overflow: hidden;
|
|
touch-action: none; /* custom pinch/pan gestures */
|
|
background: var(--ha-card-background, var(--card-background-color, #111));
|
|
}
|
|
.zoomwrap {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
.stage.mode-transition,
|
|
.stage.mode-transition .zoomwrap {
|
|
transition: none !important;
|
|
}
|
|
.stage.mode-transition .hp-paper {
|
|
fill: var(--hp-mode-paper) !important;
|
|
}
|
|
/* Sun on the plan (docs/SUN.md): the stage breathes with the day over tens
|
|
of seconds; the plan itself dims at most ~10%. Reduced motion = static. */
|
|
.stage.daynight {
|
|
transition: background-color 45s linear;
|
|
}
|
|
.stage.daynight .zoomwrap {
|
|
transition: filter 45s linear;
|
|
}
|
|
.stage.daynight.hpsettle {
|
|
transition: height 0.25s ease, background-color 45s linear;
|
|
}
|
|
/* Catch-up frame (docs/SUN.md): the sky is out of date because the card
|
|
was not painting — show the truth at once, the glide comes back on the
|
|
very next frame. */
|
|
.stage.daynight.skysnap,
|
|
.stage.daynight.skysnap .zoomwrap,
|
|
.stage.daynight.skysnap.hpsettle {
|
|
transition: none;
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.stage.daynight,
|
|
.stage.daynight .zoomwrap,
|
|
.stage.daynight.hpsettle {
|
|
transition: none;
|
|
}
|
|
}
|
|
.sunlayer {
|
|
pointer-events: none;
|
|
}
|
|
/* the compass dial in the general settings (docs/SUN.md) */
|
|
.sunrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-5);
|
|
margin: var(--sp-3) 0;
|
|
}
|
|
.suncol {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-3);
|
|
min-width: 0;
|
|
}
|
|
.compass {
|
|
width: 120px;
|
|
height: 120px;
|
|
flex: none;
|
|
touch-action: none;
|
|
cursor: grab;
|
|
user-select: none;
|
|
}
|
|
.compass:active {
|
|
cursor: grabbing;
|
|
}
|
|
.compass .cring {
|
|
fill: rgba(255, 255, 255, 0.04);
|
|
stroke: var(--divider-color, #444);
|
|
stroke-width: 2;
|
|
}
|
|
.compass .ctick {
|
|
stroke: var(--secondary-text-color, #9aa4ad);
|
|
stroke-width: 2;
|
|
}
|
|
.compass .ctick.minor {
|
|
stroke-width: 1;
|
|
opacity: 0.6;
|
|
}
|
|
.compass .cneedle line {
|
|
stroke: var(--primary-color, #3ea6ff);
|
|
stroke-width: 2.5;
|
|
stroke-linecap: round;
|
|
}
|
|
.compass .cneedle path {
|
|
fill: var(--primary-color, #3ea6ff);
|
|
}
|
|
.compass .cneedle text {
|
|
fill: var(--text-primary-color, #fff);
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
}
|
|
.compass .cdeg {
|
|
fill: var(--secondary-text-color, #9aa4ad);
|
|
font-size: 13px;
|
|
}
|
|
.compass.unset .cneedle {
|
|
opacity: 0.35;
|
|
}
|
|
/* HP-1552: first-open boot veil — the plan hides until the stage height settles */
|
|
.stage.hpboot .zoomwrap,
|
|
.stage.hpboot .zoombadge {
|
|
visibility: hidden;
|
|
}
|
|
/* 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;
|
|
}
|
|
/* docs/CANVAS.md §4.1: objects an order of magnitude away from the plan
|
|
do not decide the opening view — one quiet chip says so and offers to
|
|
take them in. Never a modal (owner). */
|
|
.farhint {
|
|
position: absolute;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
bottom: var(--sp-4);
|
|
z-index: 12;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
max-width: calc(100% - var(--sp-8));
|
|
background: var(--card-background-color, var(--hp-bg));
|
|
opacity: 0.94;
|
|
color: var(--hp-txt);
|
|
border: 1px solid var(--divider-color, #33404d);
|
|
border-radius: var(--rad-m);
|
|
padding: var(--sp-1) var(--sp-3);
|
|
font-size: var(--fs-s);
|
|
}
|
|
.farhint ha-icon {
|
|
--mdc-icon-size: 18px;
|
|
color: var(--hp-warn, #e2a03f);
|
|
flex: none;
|
|
}
|
|
.farhint span {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
/* docs/CANVAS.md §5: the plane has no edges, so you can pan until nothing
|
|
is on screen. One pointer home, one click back. */
|
|
.homearrow {
|
|
position: absolute;
|
|
z-index: 12;
|
|
transform: translate(-50%, -50%);
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
background: var(--card-background-color, var(--hp-bg));
|
|
color: var(--hp-accent);
|
|
border: 1px solid var(--hp-accent);
|
|
opacity: 0.9;
|
|
padding: 0;
|
|
}
|
|
.homearrow ha-icon {
|
|
--mdc-icon-size: 22px;
|
|
line-height: 1;
|
|
}
|
|
.zoombadge {
|
|
position: absolute;
|
|
left: var(--sp-4);
|
|
bottom: var(--sp-4);
|
|
background: var(--card-background-color, var(--hp-bg));
|
|
opacity: 0.92;
|
|
color: var(--hp-txt);
|
|
border: 1px solid var(--hp-accent);
|
|
border-radius: var(--rad-m);
|
|
padding: var(--sp-1) var(--sp-4);
|
|
font-size: var(--fs-s);
|
|
font-weight: 600;
|
|
pointer-events: none;
|
|
}
|
|
/* Only the plan canvas owns the whole zoom wrapper. Context trays now live
|
|
inside .stage as well, and may contain small SVG previews (furniture in
|
|
particular); the old descendant-wide stage-SVG selector stretched every
|
|
preview over the tray and stacked them into one blocking artefact. The
|
|
vacuum trail/fit overlays have their own explicit geometry below. */
|
|
.zoomwrap > svg {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
/* Opaque plan paper (owner 2026-08-03): the scene bg_color / daynight sky
|
|
shows ONLY around the plan, never through it. The colour is the
|
|
pre-bg_color canvas — the theme card background under an image plan,
|
|
plain white for a hand-drawn one (.stage.noplan). On drawn plans the
|
|
paper is per-room shapes following the room contours — fill only,
|
|
never a stroke, so the paper cannot poke past a wall. Night dimming
|
|
comes from the .zoomwrap brightness filter, never from alpha
|
|
(docs/SUN.md). */
|
|
.hp-paper {
|
|
fill: var(--ha-card-background, var(--card-background-color, #111));
|
|
stroke: none;
|
|
}
|
|
.stage.noplan .hp-paper,
|
|
.stage.mode-plan .hp-paper,
|
|
.stage.mode-devices .hp-paper,
|
|
.stage.mode-decor .hp-paper {
|
|
fill: #ffffff;
|
|
}
|
|
/* White day (owner 2026-08-03): at high sun the daynight sky is #ffffff —
|
|
the same white as a drawn plan's paper. A whisper of a drop shadow on
|
|
the paper GROUP keeps the sheet's contour readable on the white sky
|
|
(user units on the 1000-unit canvas ≈ a few screen px). Invisible at
|
|
night against the dark sky; 'static' mode never gets it. */
|
|
.stage.daynight .hp-paperg {
|
|
filter: drop-shadow(0 2px 8px rgba(10, 16, 26, 0.28));
|
|
}
|
|
/* Owner 2026-08-04: «углы границ комнат всё ещё с зубцами». A miter join
|
|
on a 30-45° corner shoots a spike far past the wall (and flips to an
|
|
ugly bevel once past the miter limit) — the same defect the decor lines
|
|
had before they got round caps. Every room border, in EVERY renderer
|
|
that reuses these styles (plan view, plan editor, static space-card),
|
|
joins its walls with a ROUND join instead: the corner reads as the
|
|
stroke's own radius, never as a tooth. The linecap matters only for the
|
|
open outlines below, but it costs nothing to state it here. */
|
|
.room {
|
|
transition: 0.12s;
|
|
cursor: default; /* v1.40.1: rooms are not clickable — the label's link icon is */
|
|
stroke-linejoin: round;
|
|
stroke-linecap: round;
|
|
}
|
|
.room.overlay {
|
|
fill: transparent;
|
|
stroke: transparent;
|
|
stroke-width: 2;
|
|
}
|
|
.stage.mode-view .room.overlay:not(.styled):hover {
|
|
stroke: var(--hp-accent);
|
|
stroke-opacity: 1;
|
|
}
|
|
.room.yard {
|
|
fill: rgba(75, 140, 90, 0.14);
|
|
stroke: #4b8c5a;
|
|
stroke-width: 2;
|
|
}
|
|
.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: 2.5;
|
|
fill: var(--room-fill, transparent);
|
|
fill-opacity: var(--room-fill-op, 0);
|
|
}
|
|
.glow-base-layer,
|
|
.glow-base-tunnels,
|
|
.glow-pools-frame,
|
|
.glow-pools,
|
|
.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. */
|
|
.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: 1.5;
|
|
transition: stroke-dashoffset 0.6s ease;
|
|
}
|
|
/* hover affordance: a rounded outline hugging the wall strip + a grab cursor */
|
|
.op-outline {
|
|
fill: none;
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 1.5;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
pointer-events: none;
|
|
}
|
|
.stage.markup g.opening:hover .op-outline {
|
|
opacity: 0.9;
|
|
}
|
|
/* openings are pure status graphics outside Plan mode: no cursor, no hover,
|
|
no hit target — View must not interact with them at all */
|
|
.op-hit {
|
|
fill: transparent;
|
|
pointer-events: none;
|
|
cursor: default;
|
|
}
|
|
.stage.markup .op-hit {
|
|
pointer-events: auto;
|
|
cursor: grab;
|
|
touch-action: none; /* drags, not scrolls, on touch */
|
|
}
|
|
.stage.markup .op-hit:active {
|
|
cursor: grabbing;
|
|
}
|
|
/* HP-1550-04: in the resize tool the wall handles own the hit test — the
|
|
transparent .op-hit of a door at the midpoint of a wall used to sit ON
|
|
TOP of the handle and made that wall ungrabbable for both rooms.
|
|
Openings are not editable in this tool (they ride along with the wall),
|
|
so their hit area goes fully inert; every other Plan tool is untouched. */
|
|
.stage.markup.tool-resize .op-hit {
|
|
pointer-events: none;
|
|
cursor: default;
|
|
}
|
|
.oplock {
|
|
pointer-events: none; /* inert while editing; clickable in View (rule below) */
|
|
position: absolute;
|
|
transform: translate(-50%, -50%);
|
|
width: calc(var(--icon-size, 2.5cqw) * 0.62);
|
|
height: calc(var(--icon-size, 2.5cqw) * 0.62);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--hp-bg);
|
|
border: 1px solid var(--hp-line);
|
|
z-index: 1;
|
|
}
|
|
.stage.mode-view .oplock {
|
|
pointer-events: auto;
|
|
cursor: pointer;
|
|
}
|
|
.oplock ha-icon {
|
|
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.4);
|
|
display: flex;
|
|
line-height: 0;
|
|
}
|
|
.oplock.locked { color: #66d17a; border-color: #66d17a; }
|
|
.oplock.unlocked { color: var(--hp-open); border-color: var(--hp-open); }
|
|
.oplock.unknown { color: var(--hp-muted); }
|
|
.btn.lockact {
|
|
width: 100%;
|
|
justify-content: center;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
margin-top: var(--sp-4);
|
|
}
|
|
.btn.lockact.warn {
|
|
color: var(--error-color, #d33);
|
|
border-color: var(--error-color, #d33);
|
|
}
|
|
.oprow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-4);
|
|
padding: var(--sp-3) 0;
|
|
}
|
|
.oprow b { margin-left: auto; }
|
|
.oprow.ok b { color: #66d17a; }
|
|
.oprow.warn b { color: var(--hp-open); }
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.op-leaf, .op-arc { transition: none; }
|
|
}
|
|
/* 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: calc(var(--dev-size) * var(--ripple-scale, 3));
|
|
height: calc(var(--dev-size) * var(--ripple-scale, 3));
|
|
transform: translate(-50%, -50%);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
.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 ease-out 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 + 2) { display: none; }
|
|
.device-pulse.continuous i:first-child {
|
|
animation: hp-pulse-continuous 2.4s ease-in-out infinite;
|
|
}
|
|
.device-pulse.alarm i:first-child {
|
|
border-color: #f25a4a;
|
|
animation: hp-pulse-alarm 1s ease-out infinite;
|
|
}
|
|
@keyframes hp-pulse-short {
|
|
0% { transform: scale(0.18); opacity: 0.7; }
|
|
70% { opacity: 0.22; }
|
|
100% { transform: scale(1); opacity: 0; }
|
|
}
|
|
/* Alternate identity: a rapid retrigger restarts the browser timeline. */
|
|
@keyframes hp-pulse-short-b {
|
|
0% { transform: scale(0.18); opacity: 0.7; }
|
|
70% { opacity: 0.22; }
|
|
100% { transform: scale(1); opacity: 0; }
|
|
}
|
|
@keyframes hp-pulse-continuous {
|
|
0% { transform: scale(0.26); opacity: 0.5; }
|
|
65% { opacity: 0.18; }
|
|
100% { transform: scale(1); opacity: 0; }
|
|
}
|
|
@keyframes hp-pulse-alarm {
|
|
0% { transform: scale(0.3); opacity: 0.95; }
|
|
100% { transform: scale(1); 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(--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; }
|
|
.rlgearbtn:hover { opacity: 1; filter: brightness(1.18); }
|
|
.rlgearbtn ha-icon { --mdc-icon-size: calc(var(--gear-h) * 0.55); display: inline-flex; }
|
|
.rlgear {
|
|
--mdc-icon-size: 0.9em;
|
|
display: inline-flex;
|
|
margin-right: 0.2em;
|
|
opacity: 0.6;
|
|
cursor: pointer;
|
|
pointer-events: auto;
|
|
}
|
|
.rlgear:hover { opacity: 1; }
|
|
.rlgo {
|
|
--mdc-icon-size: 0.85em;
|
|
display: inline-flex;
|
|
opacity: 0.55;
|
|
}
|
|
.stage.mode-view .rlgo {
|
|
pointer-events: auto;
|
|
cursor: pointer;
|
|
}
|
|
.stage.mode-view .rlgo:hover { opacity: 1; }
|
|
.roomlabel .rlmetrics {
|
|
position: absolute; /* below the name, outside the centring math */
|
|
top: calc(100% + 0.15em);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
white-space: nowrap;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.55em;
|
|
font-size: calc(0.75em * var(--rl-meta, 1)); /* feedback: 0.62 was unreadable on a tablet */
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
opacity: 0.9;
|
|
}
|
|
.roomlabel .rlm {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.12em;
|
|
}
|
|
.roomlabel .rlm ha-icon {
|
|
--mdc-icon-size: 1.05em;
|
|
display: inline-flex;
|
|
}
|
|
.roomlabel .rlm.lit { opacity: 1; }
|
|
.bindharow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-5);
|
|
flex-wrap: wrap;
|
|
}
|
|
.bindharow .entcheck { opacity: 0.9; }
|
|
.dropbtn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-4);
|
|
width: 100%;
|
|
text-align: left;
|
|
border: 1px solid var(--hp-muted);
|
|
border-radius: var(--rad-m);
|
|
background: transparent;
|
|
color: var(--hp-txt);
|
|
padding: var(--sp-4) 10px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: var(--fs-m);
|
|
margin-top: var(--sp-3);
|
|
}
|
|
.dropbtn .ref { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; }
|
|
.dropbtn ha-icon { --mdc-icon-size: 18px; margin-left: var(--sp-2); }
|
|
.dropbtn.open { border-color: var(--hp-accent); }
|
|
.droppanel {
|
|
border: 1px solid var(--hp-accent);
|
|
border-top: none;
|
|
border-radius: 0 0 var(--rad-m) var(--rad-m);
|
|
padding: var(--sp-3);
|
|
margin-top: -4px;
|
|
}
|
|
.ctrlchips { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin: var(--sp-2) 0; }
|
|
.ctrlchip {
|
|
display: inline-flex; align-items: center; gap: var(--sp-2);
|
|
background: var(--hp-accent); color: var(--text-primary-color, #fff);
|
|
border-radius: var(--rad-l); padding: var(--sp-2) var(--sp-4); font-size: var(--fs-s);
|
|
}
|
|
.ctrlchip ha-icon { --mdc-icon-size: 14px; cursor: pointer; }
|
|
.ctrllist { display: flex; flex-direction: column; gap: var(--sp-1); margin-top: var(--sp-2); }
|
|
.ctrlopt {
|
|
display: flex; align-items: center; gap: var(--sp-4); text-align: left;
|
|
border: 0; background: transparent; color: var(--hp-txt);
|
|
padding: var(--sp-3) var(--sp-4); border-radius: var(--rad-s); cursor: pointer; font-family: inherit; font-size: var(--fs-m);
|
|
}
|
|
.ctrlopt:hover { background: var(--secondary-background-color, rgba(128,128,128,0.15)); }
|
|
.ctrlopt .sub { color: var(--hp-muted); font-size: var(--fs-s); margin-left: auto; }
|
|
.ctrlopt ha-icon { --mdc-icon-size: 16px; }
|
|
.ctrlstates { display: flex; flex-direction: column; gap: var(--sp-2); }
|
|
.ctrlstate { display: inline-flex; align-items: center; gap: var(--sp-3); color: var(--hp-muted); }
|
|
.ctrlstate.on { color: var(--hp-txt); }
|
|
.ctrlstate ha-icon { --mdc-icon-size: 15px; }
|
|
.cardpreview {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
margin: var(--sp-4) 0 var(--sp-1);
|
|
padding: 10px;
|
|
border: 1px dashed var(--hp-muted);
|
|
border-radius: var(--rad-m);
|
|
}
|
|
.cardpreview .cpname { font-weight: 700; letter-spacing: 0.04em; }
|
|
.cardpreview .cpmeta {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.3em;
|
|
font-weight: 600;
|
|
opacity: 0.85;
|
|
}
|
|
.cardpreview .cpmeta ha-icon { --mdc-icon-size: 1.05em; }
|
|
.iconauto {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
font-size: var(--fs-s);
|
|
margin: var(--sp-2) 0 0;
|
|
}
|
|
.iconauto ha-icon { --mdc-icon-size: 18px; }
|
|
.iconauto span { flex: 1; }
|
|
.iconauto .btn { min-height: 32px; padding: 0 var(--sp-3); }
|
|
.rlhandle {
|
|
display: none;
|
|
position: absolute;
|
|
width: 9px;
|
|
height: 9px;
|
|
border-radius: 2px;
|
|
background: var(--hp-accent);
|
|
border: 1px solid var(--card-background-color, #fff);
|
|
z-index: 2;
|
|
}
|
|
.rlhandle.tl { left: -6px; top: -6px; cursor: nwse-resize; }
|
|
.rlhandle.br { right: -6px; bottom: -6px; cursor: nwse-resize; }
|
|
.rlhandle.tr { right: -6px; top: -6px; cursor: nesw-resize; }
|
|
.rlhandle.bl { left: -6px; bottom: -6px; cursor: nesw-resize; }
|
|
.stage.markup .roomlabel:hover .rlhandle { display: block; }
|
|
.stage.markup .roomlabel { pointer-events: auto; }
|
|
.roomlabel:active { cursor: grabbing; }
|
|
.measurelayer {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
.alignline {
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 1.2;
|
|
stroke-dasharray: 4 4;
|
|
pointer-events: none;
|
|
opacity: 0.9;
|
|
}
|
|
.aligndot {
|
|
fill: var(--hp-accent);
|
|
pointer-events: none;
|
|
}
|
|
.measurelabel.on45 {
|
|
color: #4bd28f;
|
|
border-color: #4bd28f;
|
|
}
|
|
.hdr.kioskhide { display: none; }
|
|
.kioskdots {
|
|
position: absolute;
|
|
left: 50%;
|
|
bottom: var(--sp-5);
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: var(--sp-4);
|
|
z-index: 5;
|
|
pointer-events: none;
|
|
}
|
|
.kdot {
|
|
width: 9px;
|
|
height: 9px;
|
|
border-radius: 50%;
|
|
background: var(--hp-muted);
|
|
opacity: 0.55;
|
|
}
|
|
.kdot.on { background: var(--hp-accent); opacity: 1; }
|
|
.measurelabel {
|
|
position: absolute;
|
|
transform: translate(12px, -150%);
|
|
font-size: var(--fs-s);
|
|
font-weight: 600;
|
|
padding: 1px var(--sp-3);
|
|
border-radius: var(--rad-s);
|
|
background: rgba(0, 0, 0, 0.72);
|
|
color: #fff;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
z-index: 3;
|
|
}
|
|
/* decor (background) layer */
|
|
.decorlayer .dshape { pointer-events: none; }
|
|
.stage.mode-decor .decorlayer .dshape {
|
|
pointer-events: visiblePainted;
|
|
cursor: pointer;
|
|
}
|
|
.stage.mode-decor.dtool-select .decorlayer .dshape { cursor: move; }
|
|
/* …but a DRAWING tool owns the canvas: existing shapes stop being targets
|
|
entirely, so a new line can start exactly on the end of an old one
|
|
instead of grabbing it (owner, 2026-08-04). Same for the picture tool —
|
|
a shape lying over the plan must not block the picture's own drag.
|
|
Only select (move) and erase (delete) keep the shapes clickable. */
|
|
.stage.mode-decor.dtool-line .decorlayer .dshape,
|
|
.stage.mode-decor.dtool-rect .decorlayer .dshape,
|
|
.stage.mode-decor.dtool-ellipse .decorlayer .dshape,
|
|
.stage.mode-decor.dtool-text .decorlayer .dshape,
|
|
.stage.mode-decor.dtool-furniture .decorlayer .dshape,
|
|
.stage.mode-decor.dtool-backdrop .decorlayer .dshape { pointer-events: none; }
|
|
/* the furniture tool is a stamp: the press must reach the stage even when
|
|
it lands on a sofa that is already there (docs/FURNITURE.md §4) */
|
|
.stage.mode-decor.dtool-furniture { cursor: copy; }
|
|
/* ONE exception (owner, 2026-08-04): under the TEXT tool an existing LABEL
|
|
is a target again — pressing it opens its editor instead of starting a
|
|
new label on top of the old one. Only labels: a line or a rectangle
|
|
under the text tool stays inert, so the press reaches the stage and a
|
|
new label is created there. */
|
|
.stage.mode-decor.dtool-text .decorlayer .dshape.dtext,
|
|
.stage.mode-decor.dtool-select .decorlayer .dshape.dtext,
|
|
.stage.mode-decor.dtool-erase .decorlayer .dshape.dtext {
|
|
/* A label is one logical decor object. SVG's visiblePainted would hit
|
|
only the ink of individual glyphs, so spaces, counters and the area
|
|
inside its selection glow behaved like empty canvas. */
|
|
pointer-events: visiblePainted;
|
|
pointer-events: bounding-box;
|
|
}
|
|
.stage.mode-decor.dtool-text .decorlayer .dshape.dtext {
|
|
cursor: text;
|
|
}
|
|
/* Erasing a hairline must not require pixel-perfect aim. The duplicate
|
|
geometry is invisible and exists only while Erase is active. A
|
|
non-scaling stroke keeps the target comfortably wide at every zoom. */
|
|
.decorlayer .derasehit {
|
|
fill: none;
|
|
stroke: transparent;
|
|
stroke-width: 16px;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
.stage.mode-decor.dtool-erase .decorlayer .derasehit {
|
|
pointer-events: stroke;
|
|
}
|
|
/* A dashed line must remain selectable across its gaps. This proxy also
|
|
makes the only entry to line-style properties — double click in Select —
|
|
practical for hairlines at every zoom. */
|
|
.decorlayer .dselecthit {
|
|
fill: none;
|
|
stroke: transparent;
|
|
stroke-width: 16px;
|
|
stroke-linecap: round;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
.stage.mode-decor.dtool-select .decorlayer .dselecthit {
|
|
pointer-events: stroke;
|
|
cursor: move;
|
|
}
|
|
.decorlayer .dsel {
|
|
filter: drop-shadow(0 0 3px var(--hp-accent));
|
|
}
|
|
.decorlayer .ddraft {
|
|
stroke-dasharray: 6 5;
|
|
pointer-events: none;
|
|
}
|
|
.decorlayer text {
|
|
font-weight: 600;
|
|
user-select: none;
|
|
dominant-baseline: middle;
|
|
text-anchor: middle;
|
|
}
|
|
.stage.mode-decor {
|
|
outline: 2px solid #26a69a;
|
|
outline-offset: -2px;
|
|
}
|
|
/* backdrop transform frame (docs/BACKDROP.md §2). Editor chrome: the
|
|
outline never takes a pointer, the four corner handles do — and they are
|
|
finger-sized (r = 2 % of the visible view), because this is dragged on a
|
|
tablet as often as with a mouse. */
|
|
.bdframe .bdbox {
|
|
fill: none;
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 2;
|
|
stroke-dasharray: 10 7;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
opacity: 0.9;
|
|
}
|
|
/* the HIT circle: invisible, finger-sized, the only thing that takes a
|
|
pointer. The visible bead is .bdknob, a quarter of its radius — the same
|
|
visual/hit split .dthandle + .dtknob and .rszhandle + .rszicon use. */
|
|
.bdframe .bdhandle {
|
|
fill: transparent;
|
|
stroke: none;
|
|
pointer-events: all;
|
|
touch-action: none;
|
|
}
|
|
.bdframe .bdknob {
|
|
fill: var(--hp-accent);
|
|
stroke: #fff;
|
|
stroke-width: 1;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
.bdframe .bdhandle:hover + .bdknob { fill: #fff; stroke: var(--hp-accent); }
|
|
.bdframe .bd-nwse { cursor: nwse-resize; }
|
|
.bdframe .bd-nesw { cursor: nesw-resize; }
|
|
/* the picture itself is the drag target for a move (grab, then grabbing) */
|
|
.stage.mode-decor.bdgrab { cursor: grab; }
|
|
.stage.mode-decor.bdgrabbing,
|
|
.stage.mode-decor.bdgrabbing .bdframe .bdhandle { cursor: grabbing; }
|
|
.measurelabel.bdmeasure {
|
|
transform: translate(-50%, -50%);
|
|
border: 1px solid var(--hp-accent);
|
|
}
|
|
/* the selected decor object's frame — same chrome rules as the backdrop's:
|
|
the outline never takes a pointer, the handles always do, and they are
|
|
finger-sized because this is dragged on a tablet too */
|
|
.dtframe .dtbox {
|
|
fill: none;
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 1.5;
|
|
stroke-dasharray: 7 5;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
opacity: 0.85;
|
|
}
|
|
.dtframe .dtstem,
|
|
.bdframe .dtstem {
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 1.5;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
opacity: 0.85;
|
|
}
|
|
/* the HIT circle: invisible, finger-sized, the only thing that takes a
|
|
pointer. The visible bead is .dtknob, a quarter of its radius — the
|
|
same visual/hit split .rszhandle + .rszicon use. */
|
|
.dtframe .dthandle {
|
|
fill: transparent;
|
|
stroke: none;
|
|
pointer-events: all;
|
|
touch-action: none;
|
|
}
|
|
.dtframe .dtknob {
|
|
fill: var(--hp-accent);
|
|
stroke: #fff;
|
|
stroke-width: 1;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
.dtframe .dthandle:hover + .dtknob { fill: #fff; stroke: var(--hp-accent); }
|
|
.dtframe .dt-nwse { cursor: nwse-resize; }
|
|
.dtframe .dt-nesw { cursor: nesw-resize; }
|
|
.dtframe .dtrot { cursor: grab; }
|
|
.dtframe .dtendpoint { cursor: crosshair; }
|
|
.bdframe .dtrot { cursor: grab; }
|
|
.dtarea {
|
|
resize: vertical;
|
|
min-height: 3.4em;
|
|
font: inherit;
|
|
line-height: 1.35;
|
|
}
|
|
.stage.mode-decor.dtool-line, .stage.mode-decor.dtool-rect,
|
|
.stage.mode-decor.dtool-ellipse, .stage.mode-decor.dtool-text {
|
|
cursor: crosshair;
|
|
}
|
|
.stage.mode-decor.dtool-erase,
|
|
.stage.mode-decor.dtool-erase .decorlayer .dshape {
|
|
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cg transform='rotate(-45 12 12)'%3E%3Crect x='7' y='2' width='10' height='18' rx='2' fill='%23fff' stroke='%23111' stroke-width='1.5'/%3E%3Cpath d='M7 13h10v5a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2z' fill='%23ff9f43' stroke='%23111' stroke-width='1.5'/%3E%3C/g%3E%3C/svg%3E") 5 22, pointer;
|
|
}
|
|
.stage.mode-decor .room, .stage.mode-decor .devlayer { pointer-events: none; }
|
|
.stage.mode-decor .oplock { pointer-events: none; }
|
|
/* 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 .rlabel,
|
|
.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;
|
|
}
|
|
.opghost {
|
|
stroke: var(--hp-open, #ff9800);
|
|
stroke-width: 5;
|
|
stroke-linecap: round;
|
|
stroke-dasharray: 7 6;
|
|
opacity: 0.85;
|
|
pointer-events: none;
|
|
}
|
|
.opghost-dot {
|
|
fill: var(--hp-open, #ff9800);
|
|
opacity: 0.85;
|
|
pointer-events: none;
|
|
}
|
|
.rlabel {
|
|
fill: var(--hp-muted);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
pointer-events: none;
|
|
text-anchor: middle;
|
|
}
|
|
.stage.edit .room {
|
|
pointer-events: none;
|
|
}
|
|
.stage.markup {
|
|
cursor: crosshair;
|
|
}
|
|
/* room-picking stages: merge (both clicks) and split before a room is chosen */
|
|
.stage.markup.tool-merge,
|
|
.stage.markup.tool-split.pickstage,
|
|
.stage.markup.tool-delroom {
|
|
cursor: pointer;
|
|
}
|
|
.stage.markup.tool-wallthick { cursor: default; }
|
|
.stage.markup.tool-wallthick.wallhot { cursor: pointer; }
|
|
/* Solid wall colour sits under the hatch (owner: both, not either/or). */
|
|
.wallbody-fill {
|
|
fill: var(--wall-fill, #ffffff);
|
|
fill-opacity: var(--wall-fill-op, 1);
|
|
fill-rule: evenodd;
|
|
stroke: none;
|
|
pointer-events: none;
|
|
}
|
|
.wallbody {
|
|
fill: url(#hp-wall-hatch);
|
|
fill-rule: evenodd;
|
|
stroke: var(--room-stroke, var(--hp-muted));
|
|
stroke-width: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
/* Thin-on-screen: hatch collapses into noise — fill alone, stroke from fill path's sibling. */
|
|
.wallbody.solid {
|
|
fill: none;
|
|
}
|
|
.wallthick-hover {
|
|
fill: var(--accent-color, #03a9f4);
|
|
fill-opacity: 0.38;
|
|
stroke: var(--accent-color, #03a9f4);
|
|
stroke-width: 2.5;
|
|
stroke-linejoin: round;
|
|
stroke-linecap: round;
|
|
pointer-events: none;
|
|
}
|
|
.wallthick-dlg {
|
|
position: absolute;
|
|
z-index: 40;
|
|
min-width: 200px;
|
|
transform: translate(-50%, 8px);
|
|
padding: 10px 12px;
|
|
border-radius: 10px;
|
|
background: var(--card-background-color, #fff);
|
|
box-shadow: 0 8px 28px rgba(0,0,0,.22);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
pointer-events: auto;
|
|
}
|
|
.wallthick-hover.isopen {
|
|
fill: var(--error-color, #f44336);
|
|
stroke: var(--error-color, #f44336);
|
|
}
|
|
.wallthick-dlg .row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.wallthick-dlg input[type="number"] {
|
|
width: 5.5em;
|
|
padding: 4px 6px;
|
|
border: 1px solid var(--divider-color, #ccc);
|
|
border-radius: 6px;
|
|
background: var(--input-fill, transparent);
|
|
color: var(--primary-text-color);
|
|
}
|
|
/* Boundary is neutral off-target, then advertises its resolved action. */
|
|
.stage.markup.tool-boundary { cursor: default; }
|
|
.stage.markup.tool-boundary.boundary-solid { cursor: crosshair; }
|
|
.stage.markup.tool-boundary.boundary-open { cursor: pointer; }
|
|
.stage.markup.tool-boundary.boundary-invalid { cursor: not-allowed; }
|
|
.openwall {
|
|
stroke: var(--ow-stroke, var(--hp-muted));
|
|
stroke-width: 2.5;
|
|
stroke-dasharray: 7 7;
|
|
stroke-linecap: butt;
|
|
pointer-events: none;
|
|
opacity: 0.9;
|
|
}
|
|
/* Rooms with open/thick stretches: the polygon's own stroke is fully off.
|
|
The trimmed .room-outline draws normal walls; View hover gets its own
|
|
top overlay after the wall bodies. */
|
|
.room.noedge {
|
|
stroke-opacity: 0 !important;
|
|
}
|
|
/* rooms with open boundaries draw their walls as separate M..L subpaths,
|
|
so a corner between two of them is two stroke ENDS meeting: round caps
|
|
fill it in the same way a round join fills a closed contour's corner
|
|
(owner 2026-08-04 — no teeth anywhere on a room border). */
|
|
.room-outline {
|
|
fill: none;
|
|
stroke-width: 2.5;
|
|
stroke-linejoin: round;
|
|
stroke-linecap: round;
|
|
pointer-events: none;
|
|
}
|
|
/* Plan editor: trimmed outlines use the markup blue */
|
|
.room-outline.outlined {
|
|
stroke: rgba(62, 166, 255, 0.55);
|
|
stroke-opacity: 1;
|
|
}
|
|
/* 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: 8;
|
|
stroke-linejoin: round;
|
|
stroke-linecap: round;
|
|
pointer-events: none;
|
|
}
|
|
.room-hover-outline {
|
|
fill: none;
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 3;
|
|
stroke-linejoin: round;
|
|
stroke-linecap: round;
|
|
pointer-events: none;
|
|
}
|
|
.openwall-preview {
|
|
stroke: #ffc14d;
|
|
stroke-width: 5;
|
|
stroke-dasharray: 7 7;
|
|
stroke-linecap: round;
|
|
pointer-events: none;
|
|
opacity: 0.95;
|
|
}
|
|
.openwall-preview.boundary-range.invalid {
|
|
stroke: var(--error-color, #f44336);
|
|
}
|
|
.openwall-preview.boundary-restore {
|
|
fill: rgba(255, 193, 77, 0.28);
|
|
fill-rule: evenodd;
|
|
stroke: #ffc14d;
|
|
stroke-width: 2.5;
|
|
stroke-dasharray: none;
|
|
stroke-linejoin: round;
|
|
}
|
|
.boundary-point {
|
|
fill: #ffc14d;
|
|
stroke: #24262d;
|
|
stroke-width: 1.5;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
.boundary-point.invalid circle {
|
|
fill: var(--error-color, #f44336);
|
|
}
|
|
.boundary-point.invalid path {
|
|
fill: none;
|
|
stroke: #fff;
|
|
stroke-width: 1.7;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
.stage.markup .room {
|
|
pointer-events: none;
|
|
}
|
|
.stage.markup .devlayer .dev {
|
|
display: none; /* in plan mode the icons do not get in the way; labels stay */
|
|
}
|
|
/* mode frames: the edit modes are visible at a glance */
|
|
.stage.mode-plan {
|
|
outline: 2px solid #ffc14d;
|
|
outline-offset: -2px;
|
|
}
|
|
.stage.mode-devices {
|
|
outline: 2px solid var(--hp-accent);
|
|
outline-offset: -2px;
|
|
}
|
|
.modes {
|
|
display: inline-flex;
|
|
gap: var(--sp-1);
|
|
background: rgba(127, 127, 127, 0.12);
|
|
border-radius: var(--rad-l);
|
|
padding: var(--sp-2);
|
|
}
|
|
.modetab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--hp-muted);
|
|
padding: var(--sp-3) 10px; /* 10px h-padding kept: +2px would wrap the header modes row */
|
|
border-radius: var(--rad-m);
|
|
font-size: var(--fs-m);
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
transition: background-color 0.14s ease, color 0.14s ease, transform 0.14s ease;
|
|
}
|
|
.modetab:active { transform: scale(0.97); }
|
|
.modetab ha-icon { --mdc-icon-size: 15px; }
|
|
.modetab .closex {
|
|
--mdc-icon-size: 13px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
margin-left: 2px;
|
|
opacity: 0.75;
|
|
cursor: pointer;
|
|
border-radius: var(--rad-s);
|
|
}
|
|
.modetab .closex:hover { opacity: 1; }
|
|
.editbar .barclose {
|
|
box-sizing: border-box;
|
|
width: 40px;
|
|
height: 40px;
|
|
padding: 0;
|
|
margin: 0;
|
|
min-width: 40px;
|
|
min-height: 40px;
|
|
justify-content: center;
|
|
gap: 0;
|
|
line-height: 0;
|
|
}
|
|
.editbar .barclose ha-icon { flex: none; margin: 0; }
|
|
.modetab.active {
|
|
background: var(--hp-accent);
|
|
color: var(--text-primary-color, #fff);
|
|
}
|
|
@media (max-width: 720px) {
|
|
.modetab .ml { display: none; }
|
|
}
|
|
.room.outlined {
|
|
stroke: rgba(62, 166, 255, 0.55);
|
|
fill: rgba(62, 166, 255, 0.06);
|
|
}
|
|
/* AFTER .outlined: same specificity — source order decides (gotcha x4) */
|
|
.room.picked {
|
|
stroke: #ffc14d;
|
|
stroke-width: 3;
|
|
fill: rgba(255, 193, 77, 0.25);
|
|
}
|
|
/* Owner 2026-08-04: the grid is a HINT, not content — at full strength the
|
|
dots argued with the plan on white paper. Both levels are muted, the
|
|
hierarchy is kept (major still denser than fine). */
|
|
.griddot {
|
|
fill: var(--hp-accent);
|
|
opacity: 0.35;
|
|
stroke: rgba(0, 0, 0, 0.35);
|
|
stroke-width: 0.4;
|
|
}
|
|
/* docs/CANVAS.md §7: every coarse node (5x/10x the live step) keeps a
|
|
bigger, more opaque dot, so zoomed far out the grid still reads as a
|
|
grid instead of a grey wash. */
|
|
.griddot.major {
|
|
opacity: 0.5;
|
|
stroke-width: 0;
|
|
}
|
|
/* the contour being drawn in the Plan editor: each wall is its own <line>,
|
|
so the round cap IS the corner (matches the finished .room border) */
|
|
.seg {
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 2.5;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
.pathline {
|
|
stroke: #ffc14d;
|
|
stroke-width: 3;
|
|
fill: none;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
.preview {
|
|
stroke: #ffc14d;
|
|
stroke-width: 2;
|
|
stroke-dasharray: 6 5;
|
|
opacity: 0.7;
|
|
}
|
|
.drawwall-preview-fill {
|
|
fill: var(--wall-fill, #ffffff);
|
|
fill-opacity: calc(var(--wall-fill-op, 1) * 0.55);
|
|
fill-rule: evenodd;
|
|
stroke: none;
|
|
pointer-events: none;
|
|
}
|
|
.drawwall-preview {
|
|
fill: url(#hp-wall-hatch);
|
|
fill-opacity: 0.55;
|
|
fill-rule: evenodd;
|
|
stroke: var(--accent-color, #03a9f4);
|
|
stroke-width: 0.5;
|
|
stroke-opacity: 0.7;
|
|
pointer-events: none;
|
|
}
|
|
.vertex {
|
|
fill: #ffc14d;
|
|
stroke: #4a2800;
|
|
stroke-width: 1;
|
|
}
|
|
.vertex.first {
|
|
fill: #4bd28f;
|
|
stroke: #04121f;
|
|
}
|
|
.areasel,
|
|
.namein {
|
|
background: var(--hp-bg);
|
|
border: 1px solid var(--hp-line);
|
|
color: var(--hp-txt);
|
|
border-radius: var(--rad-s);
|
|
padding: var(--sp-3) var(--sp-4);
|
|
font-size: var(--fs-m);
|
|
font-family: inherit;
|
|
}
|
|
.namein {
|
|
width: 130px;
|
|
}
|
|
.dev.valonly {
|
|
/* all satellite metrics from --dev-size, not --icon-size: the per-device
|
|
size multiplier must scale the value plate with its marker — same bug
|
|
class as the v1.51.3 glyph fix (pinned to base size, the multiplier
|
|
grew nothing). */
|
|
width: auto;
|
|
min-width: var(--dev-size, var(--icon-size, 2.5cqw));
|
|
max-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 4);
|
|
box-sizing: border-box;
|
|
padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16);
|
|
}
|
|
.dev.valonly .valtext {
|
|
min-width: 0;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.45);
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
}
|
|
/* RGB lights: the bulb takes the light's actual color */
|
|
/* v1.52.0: the RGB tint of the icon/border is gone — a lamp's colour
|
|
lives ONLY in its glow spot (owner's rule). The ripple-color fallback
|
|
keeps using the light colour; that is set inline via --ripple-color. */
|
|
/* Sun wedges (docs/SUN.md). The layer is present ONLY above the 3°
|
|
threshold; crossing it fades the whole layer in or out over EXACTLY
|
|
2 s (owner 2026-08-03 — RAY_FADE_MS in src/sun.ts must match). The
|
|
geometry is untouched: this is a plain opacity animation on the group,
|
|
so overlapping wedges keep their own blending while it plays. */
|
|
.sunlayer {
|
|
animation: hp-sunfade-in 2s linear both;
|
|
}
|
|
.sunlayer.out {
|
|
animation: hp-sunfade-out 2s linear both;
|
|
}
|
|
@keyframes hp-sunfade-in {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
@keyframes hp-sunfade-out {
|
|
from { opacity: 1; }
|
|
to { opacity: 0; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
/* no fade at all: the rays are simply there or simply gone */
|
|
.sunlayer, .sunlayer.out { animation: none; }
|
|
.sunlayer.out { opacity: 0; }
|
|
}
|
|
/* alarms pulse red over everything */
|
|
.dev.alarm,
|
|
.dev.alarm:hover {
|
|
background: #6f2325;
|
|
border-color: #f25a4a;
|
|
color: #fff;
|
|
box-shadow: 0 0 10px rgba(242, 90, 74, 0.65);
|
|
}
|
|
@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;
|
|
}
|
|
.dev {
|
|
position: absolute;
|
|
/* per-device multiplier on top of the card-wide icon size */
|
|
--dev-size: calc(var(--icon-size, 2.5cqw) * var(--dev-scale, 1));
|
|
/* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */
|
|
width: var(--dev-size);
|
|
height: var(--dev-size);
|
|
margin: calc(var(--dev-size) / -2 - 1px) 0 0 calc(var(--dev-size) / -2 - 1px);
|
|
border-radius: 22%;
|
|
background: var(--hp-bg);
|
|
border: 1px solid var(--hp-line);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--hp-txt);
|
|
cursor: pointer;
|
|
pointer-events: auto;
|
|
transition: background 0.15s, border-color 0.15s, opacity 0.2s;
|
|
box-shadow: var(--shadow-1);
|
|
z-index: 2;
|
|
}
|
|
.dev ha-icon {
|
|
/* from --dev-size, NOT --icon-size: the per-device size multiplier must
|
|
scale the GLYPH with its badge. Pinned to the base size, "make this
|
|
icon bigger" grew an empty box around a default-size glyph (user
|
|
report via the owner, 2026-07-29). */
|
|
--mdc-icon-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.62);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
line-height: 0;
|
|
}
|
|
.stage.mode-devices .dev { cursor: grab; }
|
|
.stage.mode-devices .dev:active { cursor: grabbing; }
|
|
.dev.on {
|
|
background: var(--hp-on);
|
|
border-color: var(--hp-on);
|
|
color: #503c00;
|
|
}
|
|
.dev.open {
|
|
background: var(--hp-open);
|
|
border-color: var(--hp-open);
|
|
color: #4a2800;
|
|
}
|
|
/* Interaction wins ordinary state colours. Alarm keeps priority through
|
|
the more-specific .dev.alarm:hover rule above. */
|
|
.dev:hover {
|
|
background: var(--hp-accent);
|
|
color: var(--text-primary-color, #fff);
|
|
z-index: 5;
|
|
}
|
|
.dev.unavail {
|
|
opacity: 0.35;
|
|
}
|
|
.physical-hit {
|
|
fill: transparent;
|
|
stroke: transparent;
|
|
pointer-events: none;
|
|
cursor: grab;
|
|
}
|
|
.stage.tool-select .physical-hit { pointer-events: all; }
|
|
.physical-hit:active { cursor: grabbing; }
|
|
line.physical-hit { cursor: pointer; }
|
|
.physical-hit.selected {
|
|
fill: rgba(255, 193, 77, 0.24);
|
|
stroke: transparent;
|
|
}
|
|
line.physical-hit.selected {
|
|
stroke: transparent;
|
|
}
|
|
.physical-drag {
|
|
fill: rgba(255, 193, 77, 0.38);
|
|
stroke: #ffc14d;
|
|
stroke-width: 2.5;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
.physical-chrome { pointer-events: none; }
|
|
.physical-chrome .frame,
|
|
.physical-chrome .stem {
|
|
fill: none;
|
|
stroke: #ffc14d;
|
|
stroke-width: 2;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
.physical-chrome .frame { fill: rgba(255, 193, 77, 0.22); }
|
|
.physical-chrome polyline.frame { fill: none; }
|
|
.physical-chrome .move-dot {
|
|
fill: #ffc14d;
|
|
stroke: #24262d;
|
|
stroke-width: 1.5;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
.physical-chrome .rotate-handle {
|
|
fill: #24262d;
|
|
stroke: #ffc14d;
|
|
stroke-width: 2.5;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: all;
|
|
cursor: crosshair;
|
|
}
|
|
.drawwall.invalid input { border-color: var(--error-color, #db4437); }
|
|
.drawwall .rangehint { margin-inline-start: 4px; font-size: 0.78em; opacity: 0.72; }
|
|
.drawwall.invalid .rangehint { color: var(--error-color, #db4437); opacity: 1; }
|
|
.stage.markup.tool-partition,
|
|
.stage.markup.tool-column { cursor: crosshair; }
|
|
.dev.virtual {
|
|
border-style: dashed;
|
|
}
|
|
/* "hide from plan" flag, shown only in the device editor with the
|
|
"show hidden devices" toggle on (docs/FILTERING.md). BLUE, so a hidden
|
|
device cannot be mistaken for an unavailable one (translucent dark) —
|
|
and no live-state paint at all: a ghost is configuration, not status
|
|
(owner's request). */
|
|
.dev.ghost {
|
|
opacity: 0.6;
|
|
border-style: dashed;
|
|
border-color: var(--hp-accent);
|
|
background: rgba(62, 166, 255, 0.22); /* fallback for old WebViews */
|
|
background: color-mix(in srgb, var(--hp-accent) 30%, var(--card-background-color, #1c2530));
|
|
color: var(--hp-accent);
|
|
box-shadow: none;
|
|
}
|
|
/* HA-disabled is not a user hide: neutral grey + a power-off badge keeps
|
|
the two ghosts distinguishable without relying on colour alone. */
|
|
.dev.ghost.ha-disabled {
|
|
opacity: 0.62;
|
|
border-color: var(--secondary-text-color, #9aa0aa);
|
|
background: rgba(120, 124, 134, 0.2);
|
|
background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 24%, var(--card-background-color, #1c2530));
|
|
color: var(--secondary-text-color, #9aa0aa);
|
|
}
|
|
.dev .habadge {
|
|
position: absolute;
|
|
right: -18%;
|
|
bottom: -18%;
|
|
display: grid;
|
|
place-items: center;
|
|
width: 46%;
|
|
height: 46%;
|
|
min-width: 12px;
|
|
min-height: 12px;
|
|
border-radius: 50%;
|
|
background: var(--card-background-color, #242832);
|
|
border: 1px solid currentColor;
|
|
}
|
|
.dev .habadge ha-icon { width: 72%; height: 72%; }
|
|
.habindingbanner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
padding: var(--sp-3) var(--sp-4);
|
|
margin-bottom: var(--sp-4);
|
|
border: 1px solid var(--warning-color, #ff9800);
|
|
border-radius: var(--rad-m);
|
|
background: color-mix(in srgb, var(--warning-color, #ff9800) 12%, transparent);
|
|
color: var(--primary-text-color, #f1f3f6);
|
|
}
|
|
.habindingbanner > span { flex: 1; min-width: 0; }
|
|
.habindingbanner > ha-icon { color: var(--warning-color, #ff9800); flex: 0 0 auto; }
|
|
.habindingbanner.limited {
|
|
border-color: var(--secondary-text-color, #9aa0aa);
|
|
background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 10%, transparent);
|
|
}
|
|
.habindingbanner.limited > ha-icon { color: var(--secondary-text-color, #9aa0aa); }
|
|
.dev.sel {
|
|
border-color: #ffc14d;
|
|
box-shadow: 0 0 0 3px rgba(255, 193, 77, 0.35);
|
|
}
|
|
|
|
.dev .value-badge {
|
|
position: absolute;
|
|
z-index: 2;
|
|
box-sizing: border-box;
|
|
max-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 4);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
background: var(--card-background-color, var(--hp-bg));
|
|
border: 1px solid var(--hp-accent, #ff9800);
|
|
border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.18);
|
|
padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14);
|
|
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.45);
|
|
font-weight: 700;
|
|
line-height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.68);
|
|
color: var(--hp-txt);
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
}
|
|
.dev .value-badge.tone-humidity { border-color: #4fc3f7; }
|
|
.dev .value-badge.unavailable,
|
|
.dev .value-badge.missing { opacity: 0.66; }
|
|
.dev .value-badge.pos-right {
|
|
left: 100%;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
margin-left: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1);
|
|
}
|
|
.dev .value-badge.pos-left {
|
|
right: 100%;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
margin-right: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1);
|
|
}
|
|
.dev .value-badge.pos-top {
|
|
left: 50%;
|
|
bottom: 100%;
|
|
transform: translateX(-50%);
|
|
margin-bottom: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1);
|
|
}
|
|
.dev .value-badge.pos-bottom {
|
|
left: 50%;
|
|
top: 100%;
|
|
transform: translateX(-50%);
|
|
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.1);
|
|
}
|
|
.dev .lqi {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.05);
|
|
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.38);
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9);
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
}
|
|
.dev .lqi.below-value-badge {
|
|
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.88);
|
|
}
|
|
.editbar {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
align-items: stretch;
|
|
border-bottom: 1px solid var(--hp-line);
|
|
font-size: var(--fs-m);
|
|
}
|
|
.editbar-tools {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: var(--sp-4) var(--sp-5);
|
|
flex-wrap: wrap;
|
|
min-width: 0;
|
|
outline: none;
|
|
}
|
|
.editbar-end {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: var(--sp-4) var(--sp-5);
|
|
border-inline-start: 1px solid var(--hp-line);
|
|
background: var(--card-background-color, var(--hp-bg));
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
.tab .tabedit {
|
|
--mdc-icon-size: 13px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
margin-left: var(--sp-3);
|
|
opacity: 0.4;
|
|
}
|
|
.tab:hover .tabedit {
|
|
opacity: 0.9;
|
|
}
|
|
.tab.tabadd {
|
|
padding: var(--sp-3) var(--sp-4);
|
|
}
|
|
.tab.tabadd ha-icon {
|
|
--mdc-icon-size: 15px;
|
|
}
|
|
.srcrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-4);
|
|
font-size: var(--fs-m);
|
|
cursor: pointer;
|
|
padding: var(--sp-1) 0;
|
|
}
|
|
.dispsection {
|
|
margin-top: var(--sp-5) !important;
|
|
padding-top: var(--sp-4);
|
|
border-top: 1px solid var(--hp-line);
|
|
font-weight: 600;
|
|
color: var(--hp-txt) !important;
|
|
}
|
|
.colorrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-4);
|
|
}
|
|
.colorrow input[type='color'] {
|
|
width: 42px;
|
|
height: 28px;
|
|
border: 1px solid var(--hp-line);
|
|
border-radius: var(--rad-s);
|
|
background: transparent;
|
|
padding: 1px;
|
|
cursor: pointer;
|
|
}
|
|
.colorrow input[type='range'] { flex: 1; }
|
|
.colorrow .tempin { width: 70px; flex: none; }
|
|
.temprange {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
margin-left: auto;
|
|
color: var(--hp-muted);
|
|
font-size: var(--fs-s);
|
|
}
|
|
/* beat the generic hp-dialog .body .namein { width:100% } rule */
|
|
hp-dialog .body .temprange .tempin { width: 56px; flex: none; padding: var(--sp-2) var(--sp-3); }
|
|
hp-dialog .body .colorrow .tempin { width: 72px; flex: none; }
|
|
.srcrow { flex-wrap: nowrap; }
|
|
/* native HA controls (rendered only when the HA frontend defines them;
|
|
old HA and the smoke env keep the plain inputs). ha-switch is taller
|
|
than a checkbox - cap its footprint so .srcrow keeps its rhythm. */
|
|
.srcrow ha-switch { flex: none; }
|
|
.colorrow ha-slider { flex: 1; min-width: 0; }
|
|
.srcrow > span:first-of-type { white-space: nowrap; }
|
|
.colorrow .opl { color: var(--hp-muted); font-size: var(--fs-s); }
|
|
.colorrow .opv { font-size: var(--fs-s); min-width: 34px; text-align: right; }
|
|
.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(--dev-size, var(--icon-size, 2.5cqw)) * 0.8);
|
|
width: var(--puck-size);
|
|
height: var(--puck-size);
|
|
border-radius: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: var(--hp-bg);
|
|
border: 1px solid var(--hp-line);
|
|
box-shadow: var(--shadow-1);
|
|
color: var(--hp-txt);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
z-index: 6;
|
|
/* glide between sparse position updates; .jump disables it so the robot
|
|
never appears to drive through walls after a data gap */
|
|
transition: left 1.2s linear, top 1.2s linear;
|
|
animation: vacpulse 2.2s ease-out infinite;
|
|
}
|
|
.vacpuck.jump { transition: none; }
|
|
.vacpuck.stale { opacity: 0.45; animation: none; }
|
|
.vacpuck ha-icon {
|
|
--mdc-icon-size: calc(var(--puck-size) * 0.68);
|
|
color: var(--hp-txt);
|
|
/* same centering recipe as .dev ha-icon: without flex + line-height 0
|
|
the glyph sits on its text baseline and appears to float around the
|
|
circle (owner report 2026-07-31) */
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
line-height: 0;
|
|
width: var(--mdc-icon-size);
|
|
height: var(--mdc-icon-size);
|
|
}
|
|
@keyframes vacpulse {
|
|
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hp-accent) 45%, transparent); }
|
|
70% { box-shadow: 0 0 0 12px transparent; }
|
|
100% { box-shadow: 0 0 0 0 transparent; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.vacpuck { animation: none; }
|
|
}
|
|
.vactrail {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 5;
|
|
overflow: visible;
|
|
}
|
|
.vactrail polyline,
|
|
.vactrail path {
|
|
fill: none;
|
|
stroke-linejoin: round;
|
|
stroke-linecap: round;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
/* dark halo + light core: neutral, and one of the two always contrasts
|
|
with whatever fill is underneath */
|
|
.vactrail g.prev { opacity: 0.4; }
|
|
.vactrail .case {
|
|
stroke: rgba(0, 0, 0, 0.4);
|
|
stroke-width: 2.25;
|
|
}
|
|
.vactrail .core {
|
|
stroke: rgba(255, 255, 255, 0.82);
|
|
stroke-width: 0.9;
|
|
}
|
|
.vacbox .vacbtns { display: flex; gap: var(--sp-4); margin: var(--sp-3) 0; flex-wrap: wrap; }
|
|
.vacdiag { display: grid; gap: 4px; margin-bottom: var(--sp-3); }
|
|
.vacdiag > div { display: flex; justify-content: space-between; gap: var(--sp-5); }
|
|
.vacdiag > div > span { color: var(--secondary-text-color); }
|
|
.vacdiag > div > b { text-align: right; overflow-wrap: anywhere; }
|
|
.vacpicker { margin: var(--sp-3) 0; }
|
|
.vacsource-warning { display: grid; gap: 8px; }
|
|
.vacsource-warning .btn { justify-self: start; }
|
|
.vacpicker > summary { display: inline-flex; width: auto; list-style: none; cursor: pointer; }
|
|
.vacpicker > summary::-webkit-details-marker { display: none; }
|
|
.vacsource-list { display: grid; gap: 6px; margin-top: 8px; }
|
|
.vacsource-list details { padding: 6px 0 0; }
|
|
.vacsource-list details > summary { cursor: pointer; font-weight: 600; }
|
|
.vacsource { display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
width: 100%; min-width: 0; padding: 9px 10px; border: 1px solid var(--divider-color);
|
|
border-radius: 10px; color: var(--primary-text-color); background: var(--secondary-background-color);
|
|
text-align: left; cursor: pointer; }
|
|
.vacsource.on { border-color: var(--accent-color); box-shadow: inset 3px 0 var(--accent-color); }
|
|
.vacsource > span:first-child { min-width: 0; display: grid; gap: 2px; }
|
|
.vacsource small { color: var(--secondary-text-color); overflow-wrap: anywhere; }
|
|
.vacsource-meta { color: var(--secondary-text-color); text-align: right; font-size: 0.82em; }
|
|
.vacxcme pre { margin: 8px 0 0; white-space: pre-wrap; user-select: text; }
|
|
.vacfit {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 12;
|
|
overflow: visible;
|
|
touch-action: none;
|
|
cursor: grab;
|
|
/* the devlayer is pointer-events: none and every child opts back in —
|
|
without this line real clicks flew straight through the overlay
|
|
(owner: «уголки не кликабельны»; synthetic smoke events bypass
|
|
hit-testing, which is why they lied) */
|
|
pointer-events: auto;
|
|
}
|
|
.vacfit:active { cursor: grabbing; }
|
|
.vacfit polygon {
|
|
fill: color-mix(in srgb, var(--hp-accent) 16%, transparent);
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 2;
|
|
vector-effect: non-scaling-stroke;
|
|
stroke-dasharray: 6 4;
|
|
}
|
|
.vacfit text {
|
|
fill: var(--hp-accent);
|
|
font-size: 26px;
|
|
text-anchor: middle;
|
|
dominant-baseline: middle;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
/* room resize tool (docs/RESIZE.md) */
|
|
/* wall handle: invisible finger-sized hit circle (HP-1550-04 hit priority
|
|
kept), the visible glyph lives in the sibling .rszicon */
|
|
.rszhandle {
|
|
fill: transparent;
|
|
stroke: none;
|
|
pointer-events: all;
|
|
cursor: grab;
|
|
touch-action: none;
|
|
}
|
|
.rszhandle:active { cursor: grabbing; }
|
|
/* wall-with-arrows glyph: accent ink over a bg halo, readable on any plan */
|
|
.rszicon { pointer-events: none; }
|
|
.rszicon path {
|
|
fill: none;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
.rszhalo { stroke: var(--hp-bg); stroke-width: 6; }
|
|
.rszink { stroke: var(--hp-accent); stroke-width: 2; }
|
|
.rszhandle:hover + .rszicon .rszink { stroke-width: 3; }
|
|
/* corner (scale-frame) handles: hit circle invisible, .rszknob is the bead */
|
|
.rszcorner {
|
|
fill: transparent;
|
|
stroke: none;
|
|
cursor: nwse-resize;
|
|
}
|
|
.rszknob {
|
|
fill: var(--hp-bg);
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 2;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
.rszcorner:hover + .rszknob { fill: var(--hp-accent); }
|
|
.rszcorner:active { cursor: nwse-resize; }
|
|
.rszframe {
|
|
fill: none;
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 1.5;
|
|
stroke-dasharray: 6 5;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
/* the decor draft badge rides the MIDDLE of the shape, so it is centred
|
|
horizontally and lifted clear of the line instead of trailing the
|
|
cursor the way a wall badge does (owner 2026-08-04) */
|
|
.measurelabel.dmeasure {
|
|
transform: translate(-50%, -160%);
|
|
}
|
|
.measurelabel.rszarea {
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(0, 0, 0, 0.6);
|
|
}
|
|
/* width and depth of a piece of furniture while its corner is dragged —
|
|
centred on the edge they measure (docs/FURNITURE.md §6) */
|
|
.measurelabel.furnmeasure {
|
|
transform: translate(-50%, -50%);
|
|
border: 1px solid var(--hp-accent);
|
|
}
|
|
/* ---- the furniture palette (docs/FURNITURE.md §3) ------------------- */
|
|
.furnpalette {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: 38vh;
|
|
border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12));
|
|
background: var(--card-background-color, var(--hp-bg));
|
|
font-size: 0.85em;
|
|
}
|
|
.furnhd {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 8px;
|
|
font-weight: 600;
|
|
opacity: 0.9;
|
|
}
|
|
.furnhd .spacer { flex: 1; }
|
|
.furnbody {
|
|
overflow: auto;
|
|
padding: 0 8px 6px;
|
|
}
|
|
.furngroup {
|
|
margin: 6px 0 3px;
|
|
font-size: 0.85em;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
opacity: 0.6;
|
|
}
|
|
.furnrow {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
}
|
|
.furnitem {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 2px;
|
|
width: 74px;
|
|
padding: 4px 2px;
|
|
border: 1px solid transparent;
|
|
border-radius: 8px;
|
|
background: rgba(127, 127, 127, 0.08);
|
|
color: inherit;
|
|
font: inherit;
|
|
font-size: 0.8em;
|
|
line-height: 1.15;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
}
|
|
.furnitem:hover { background: rgba(127, 127, 127, 0.18); }
|
|
.furnitem.on {
|
|
border-color: var(--hp-accent);
|
|
background: rgba(38, 166, 154, 0.18);
|
|
}
|
|
.furnprev {
|
|
width: 40px;
|
|
height: 40px;
|
|
color: var(--primary-text-color, currentColor);
|
|
}
|
|
.furnsize {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
padding: 6px 8px;
|
|
border-top: 1px solid var(--hp-border, rgba(255, 255, 255, 0.12));
|
|
}
|
|
.furnsize label {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 3px;
|
|
opacity: 0.8;
|
|
}
|
|
.furnsize .furnunit { opacity: 0.6; font-size: 0.85em; }
|
|
.furnsize input {
|
|
width: 5.5em;
|
|
padding: 3px 5px;
|
|
}
|
|
.furnhint { opacity: 0.6; }
|
|
.vacfitdot { fill: var(--hp-accent); pointer-events: none; }
|
|
/* hit target: invisible and finger-sized; .vacfitknob is the visible bead */
|
|
.vacfithandle {
|
|
fill: transparent;
|
|
stroke: none;
|
|
cursor: nwse-resize;
|
|
}
|
|
.vacfitknob {
|
|
fill: var(--hp-bg);
|
|
stroke: var(--hp-accent);
|
|
stroke-width: 2;
|
|
vector-effect: non-scaling-stroke;
|
|
pointer-events: none;
|
|
}
|
|
.vaccalbar {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: 24px;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: var(--sp-5);
|
|
align-items: center;
|
|
background: var(--hp-bg);
|
|
color: var(--hp-txt);
|
|
border: 1px solid var(--hp-accent);
|
|
border-radius: var(--rad-l);
|
|
padding: 10px var(--sp-5);
|
|
z-index: 60;
|
|
box-shadow: var(--shadow-2);
|
|
}
|
|
.candlist {
|
|
max-height: 160px;
|
|
overflow-y: auto;
|
|
border-top: 1px solid var(--hp-line);
|
|
/* A scrollable box is a flex item that HAPPILY collapses: inside the
|
|
dialog body (a flex column) this list rendered its rows into a 1px
|
|
sliver — the DOM had 26 candidates and the user saw nothing. In the
|
|
binding dropdown it sits inside .droppanel (block context) and never
|
|
showed the bug. Field report, 2026-07-30. */
|
|
flex: 0 0 auto;
|
|
min-height: 2.6em;
|
|
}
|
|
.cand {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: var(--sp-4);
|
|
padding: var(--sp-3) var(--sp-4);
|
|
cursor: pointer;
|
|
border-radius: var(--rad-s);
|
|
font-size: var(--fs-m);
|
|
}
|
|
.cand:hover {
|
|
background: rgba(127, 127, 127, 0.15);
|
|
}
|
|
.cand.sel {
|
|
background: var(--hp-accent);
|
|
color: var(--text-primary-color, #fff);
|
|
}
|
|
.cand .cs {
|
|
color: var(--hp-muted);
|
|
font-size: var(--fs-s);
|
|
white-space: nowrap;
|
|
}
|
|
.cand.sel .cs {
|
|
color: var(--text-primary-color, #fff);
|
|
opacity: 0.85;
|
|
}
|
|
.cand.muted {
|
|
color: var(--hp-muted);
|
|
cursor: default;
|
|
}
|
|
.pdfedit {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--sp-3);
|
|
align-items: center;
|
|
}
|
|
.pdftag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
border: 1px solid var(--hp-line);
|
|
border-radius: var(--rad-s);
|
|
padding: var(--sp-2) var(--sp-3);
|
|
font-size: var(--fs-s);
|
|
}
|
|
.pdftag a {
|
|
color: var(--hp-txt);
|
|
text-decoration: none;
|
|
max-width: 150px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.pdftag .x {
|
|
--mdc-icon-size: 15px;
|
|
cursor: pointer;
|
|
color: var(--hp-muted);
|
|
}
|
|
.pdftag .x:hover {
|
|
color: #ff7a5c;
|
|
}
|
|
.entlist {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-2);
|
|
margin-bottom: 10px;
|
|
}
|
|
.entrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-4);
|
|
padding: var(--sp-3) var(--sp-4);
|
|
border-radius: var(--rad-m);
|
|
background: var(--secondary-background-color, rgba(128, 128, 128, 0.12));
|
|
}
|
|
.entrow ha-icon { --mdc-icon-size: 20px; color: var(--hp-muted); }
|
|
.entrow.on ha-icon { color: var(--hp-accent); }
|
|
.entrow .en { flex: 1; font-size: var(--fs-m); }
|
|
.entrow .ev { font-size: var(--fs-m); color: var(--hp-muted); }
|
|
.entbtn {
|
|
min-width: 74px;
|
|
min-height: 32px;
|
|
padding: var(--sp-2) var(--sp-5);
|
|
border: 1px solid var(--hp-muted);
|
|
border-radius: 999px;
|
|
background: transparent;
|
|
color: var(--hp-txt);
|
|
font: inherit;
|
|
font-size: var(--fs-m);
|
|
cursor: pointer;
|
|
}
|
|
.entbtn.on {
|
|
background: var(--hp-accent);
|
|
border-color: var(--hp-accent);
|
|
color: var(--text-primary-color, #fff);
|
|
font-weight: 600;
|
|
}
|
|
.inforow {
|
|
display: flex;
|
|
gap: 10px;
|
|
font-size: var(--fs-m);
|
|
margin: var(--sp-2) 0;
|
|
}
|
|
.inforow .k {
|
|
color: var(--hp-muted);
|
|
min-width: 84px;
|
|
}
|
|
.inforow a {
|
|
color: var(--hp-accent);
|
|
word-break: break-all;
|
|
}
|
|
.infodesc {
|
|
font-size: var(--fs-m);
|
|
white-space: pre-wrap;
|
|
margin-top: var(--sp-3);
|
|
}
|
|
.infodesc.muted {
|
|
color: var(--hp-muted);
|
|
}
|
|
.pdflist {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-2);
|
|
}
|
|
.pdf {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
color: var(--hp-accent);
|
|
text-decoration: none;
|
|
}
|
|
ha-icon-picker {
|
|
display: block;
|
|
}
|
|
.floorrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-4);
|
|
padding: var(--sp-3) var(--sp-2);
|
|
font-size: var(--fs-m);
|
|
cursor: pointer;
|
|
}
|
|
.floorrow .floorlvl {
|
|
color: var(--hp-muted);
|
|
font-size: var(--fs-s);
|
|
border: 1px solid var(--hp-line);
|
|
border-radius: var(--rad-s);
|
|
padding: 0 var(--sp-3);
|
|
}
|
|
.rhint {
|
|
font-size: var(--fs-s);
|
|
color: var(--hp-muted);
|
|
margin-bottom: var(--sp-3);
|
|
}
|
|
.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;
|
|
}
|
|
.rrow .ract:hover { color: var(--hp-txt); }
|
|
.rrow .ract.del:hover { color: #ff7a5c; }
|
|
|
|
.gsrow .gsl {
|
|
min-width: 150px;
|
|
font-size: var(--fs-m);
|
|
color: var(--hp-muted);
|
|
}
|
|
.alignmsg { margin: 0 0 8px; font-size: 13px; line-height: 1.45; }
|
|
.btn.alignall { width: 100%; justify-content: center; }
|
|
.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; }
|
|
.backupfile, .backupsummary, .backupcontent {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
gap: var(--sp-1);
|
|
}
|
|
.backupfile b, .backupcontent span {
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.backupfile span, .backupsummary span, .backupcontent span {
|
|
color: var(--hp-muted);
|
|
font-size: var(--fs-s);
|
|
}
|
|
.backupcounts {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: var(--sp-2) var(--sp-4);
|
|
font-size: var(--fs-s);
|
|
}
|
|
.backupwarn, .backuperror {
|
|
border-radius: var(--rad-s);
|
|
padding: var(--sp-3);
|
|
font-size: var(--fs-s);
|
|
line-height: 1.45;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.backupwarn { background: color-mix(in srgb, var(--hp-accent) 12%, transparent); }
|
|
.backuperror { background: rgba(179, 64, 42, .16); color: #ff7a5c; }
|
|
.backupchoices {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-2);
|
|
border: 1px solid var(--hp-line);
|
|
border-radius: var(--rad-s);
|
|
min-width: 0;
|
|
}
|
|
.backupchoices label { margin: 0 !important; display: flex; gap: var(--sp-2); }
|
|
.backupconfirm { align-items: flex-start !important; }
|
|
.backupconfirm > span:first-of-type {
|
|
min-width: 0;
|
|
white-space: normal;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
@media (max-width: 520px) {
|
|
.backupactions, .backupcounts { grid-template-columns: 1fr; }
|
|
}
|
|
.aboutver {
|
|
font-size: var(--fs-s);
|
|
color: var(--hp-muted);
|
|
margin: var(--sp-2) 0 var(--sp-3);
|
|
}
|
|
.aboutlink {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
width: fit-content;
|
|
color: var(--hp-accent);
|
|
text-decoration: none;
|
|
font-size: var(--fs-m);
|
|
padding: var(--sp-1) 0;
|
|
}
|
|
.aboutlink:hover { text-decoration: underline; }
|
|
.aboutlink ha-icon { --mdc-icon-size: 18px; line-height: 1; }
|
|
hp-dialog .body {
|
|
padding: var(--sp-5) var(--sp-6);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--sp-3);
|
|
}
|
|
hp-dialog .body label {
|
|
font-size: var(--fs-s);
|
|
color: var(--hp-muted);
|
|
margin-top: var(--sp-3);
|
|
}
|
|
hp-dialog .body .namein,
|
|
hp-dialog .body .areasel {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
/* Room settings contains long radio labels, two source pickers and live
|
|
previews, so it deliberately uses hp-dialog's medium width. Keep its
|
|
content shrinkable as well: the generic srcrow rule is nowrap because
|
|
compact switch rows need it, but here that created a horizontal scroll
|
|
box even on a wide desktop viewport. */
|
|
hp-dialog.roomdialog .body {
|
|
min-width: 0;
|
|
overflow-x: hidden;
|
|
}
|
|
hp-dialog.roomdialog .body > * {
|
|
min-width: 0;
|
|
max-width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
hp-dialog.roomdialog .srcrow {
|
|
min-width: 0;
|
|
align-items: flex-start;
|
|
}
|
|
hp-dialog.roomdialog .srcrow > span:first-of-type {
|
|
min-width: 0;
|
|
white-space: normal;
|
|
overflow-wrap: anywhere;
|
|
line-height: 1.35;
|
|
}
|
|
hp-dialog.roomdialog .dropbtn > b,
|
|
hp-dialog.roomdialog .dropbtn > .ref {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.devicepreview-empty {
|
|
min-height: 82px;
|
|
margin: 4px 0 14px;
|
|
padding: var(--sp-4);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--sp-3);
|
|
border: 1px dashed var(--hp-line);
|
|
border-radius: var(--rad-m);
|
|
color: var(--hp-muted);
|
|
text-align: center;
|
|
}
|
|
hp-dialog .row {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: var(--sp-4);
|
|
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;
|
|
}
|
|
.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);
|
|
}
|
|
.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;
|
|
}
|
|
`;
|