mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 04:38:55 +00:00
591 lines
21 KiB
TypeScript
591 lines
21 KiB
TypeScript
/** Device markers, shells, pulses and vacuum presentation (#266, split from styles.ts). */
|
|
import { css } from 'lit';
|
|
|
|
export const devicesStyles = css`
|
|
/* Unified device activity: alarm, short event and continuous state all
|
|
share this one renderer. No static rings are allowed. */
|
|
.dev ha-icon {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
.device-pulse {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: var(--dev-size);
|
|
height: var(--dev-size);
|
|
transform: translate(-50%, -50%);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
.device-pulse i {
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--ripple-color, var(--hp-accent));
|
|
opacity: 0;
|
|
}
|
|
/* A witnessed edge: exactly three waves over the 3.3 s runtime window. */
|
|
.device-pulse.short i {
|
|
animation: hp-pulse-short 1.1s cubic-bezier(.22,.61,.36,1) 1 forwards;
|
|
}
|
|
.device-pulse.short i:nth-child(2) { animation-delay: 1.1s; }
|
|
.device-pulse.short i:nth-child(3) { animation-delay: 2.2s; }
|
|
.device-pulse.short.gen2 i { animation-name: hp-pulse-short-b; }
|
|
.device-pulse.continuous i:nth-child(n + 2),
|
|
.device-pulse.alarm i:nth-child(n + 3) { display: none; }
|
|
.device-pulse.continuous i:first-child {
|
|
animation: hp-pulse-continuous 3.6s cubic-bezier(.45,.05,.55,.95) infinite;
|
|
}
|
|
.device-pulse.alarm i {
|
|
border-width: 3px;
|
|
border-color: #F0410C;
|
|
animation: hp-pulse-alarm 2.4s cubic-bezier(.22,.61,.36,1) infinite;
|
|
}
|
|
.device-pulse.alarm i:nth-child(2) { animation-delay: 1.2s; }
|
|
.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;
|
|
}
|
|
.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; }
|
|
.dev.valonly {
|
|
/* Saved coordinates remain the centre of a Text shell. The shell may
|
|
expand, while the invisible anchor stays one core diameter wide. */
|
|
width: var(--dev-size, var(--icon-size, 2.5cqw));
|
|
}
|
|
.dev.valonly .device-core {
|
|
width: max-content;
|
|
min-width: var(--dev-size, var(--icon-size, 2.5cqw));
|
|
padding-inline: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.16);
|
|
border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) / 2);
|
|
}
|
|
.dev .valtext {
|
|
overflow: visible;
|
|
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45));
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
@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;
|
|
}
|
|
.dev {
|
|
position: absolute;
|
|
/* The surface resolves compatibility icon_size units once. The shared
|
|
face receives an effective base and only applies the per-device size. */
|
|
--dev-size: calc(var(--device-base-size, 2.25cqw) * var(--dev-scale, 1));
|
|
/* 101.5/80 is the package shell/core ratio, including its stroke. */
|
|
--device-shell-size: calc(var(--dev-size) * 1.26875);
|
|
--device-shell-inset: calc(var(--dev-size) * 0.134375);
|
|
--device-shell-stroke-ratio: 0.01875;
|
|
--device-shell-border-width: max(1px, calc(var(--dev-size) * var(--device-shell-stroke-ratio)));
|
|
--device-core-bg: var(--card-background-color, #fff);
|
|
--device-core-fg: var(--primary-text-color, #252525);
|
|
--device-core-bg: light-dark(#fff, #252525);
|
|
--device-core-fg: light-dark(#252525, #fff);
|
|
--device-face-bg: var(--device-core-bg);
|
|
--device-face-fg: var(--device-core-fg);
|
|
--device-shell-base-stroke: light-dark(#BCBCBC, rgb(37 37 37 / 75%));
|
|
--device-shell-stroke: var(--device-shell-base-stroke);
|
|
--device-shell-shadow:
|
|
0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%),
|
|
0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175)
|
|
calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%);
|
|
--device-core-inset-shadow: 0 0 0 0 transparent;
|
|
--device-ring-color: transparent;
|
|
--device-ring-width: 0px;
|
|
/* The saved point is the exact centre of the icon core. */
|
|
width: var(--dev-size);
|
|
height: var(--dev-size);
|
|
margin: calc(var(--dev-size) / -2) 0 0 calc(var(--dev-size) / -2);
|
|
border: 0;
|
|
background: transparent;
|
|
display: block;
|
|
color: var(--device-core-fg);
|
|
cursor: pointer;
|
|
pointer-events: auto;
|
|
transition: opacity 0.2s;
|
|
box-shadow: none;
|
|
outline: none;
|
|
z-index: 2;
|
|
}
|
|
.dev.theme-light {
|
|
--device-core-bg: #fff;
|
|
--device-core-fg: #000;
|
|
--device-shell-base-stroke: #BCBCBC;
|
|
--device-shell-stroke: var(--device-shell-base-stroke);
|
|
--device-shell-shadow:
|
|
0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .05) rgb(37 40 45 / 12%),
|
|
0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175)
|
|
calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%);
|
|
--device-core-inset-shadow: 0 0 0 0 transparent;
|
|
}
|
|
.dev.theme-dark {
|
|
--device-core-bg: #252525;
|
|
--device-core-fg: #fff;
|
|
--device-shell-base-stroke: rgb(37 37 37 / 75%);
|
|
--device-shell-stroke: var(--device-shell-base-stroke);
|
|
--device-shell-shadow:
|
|
0 calc(var(--dev-size) * .025) calc(var(--dev-size) * .0375) rgb(37 40 45 / 12%),
|
|
0 calc(var(--dev-size) * .1) calc(var(--dev-size) * .175)
|
|
calc(var(--dev-size) * -.025) rgb(37 40 45 / 18%);
|
|
--device-core-inset-shadow:
|
|
inset 0 calc(var(--dev-size) * 0.0125) calc(var(--dev-size) * 0.0125) rgb(255 255 255 / 70%);
|
|
}
|
|
.dev::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: max(44px, var(--device-shell-size));
|
|
height: max(44px, var(--device-shell-size));
|
|
transform: translate(-50%, -50%);
|
|
border-radius: 50%;
|
|
pointer-events: auto;
|
|
z-index: 3;
|
|
}
|
|
.device-shell {
|
|
position: absolute;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(var(--dev-size) * 0.1);
|
|
padding: 0;
|
|
min-width: var(--dev-size);
|
|
min-height: var(--dev-size);
|
|
border: 0;
|
|
background: transparent;
|
|
box-shadow: none;
|
|
transition: opacity .2s;
|
|
pointer-events: none;
|
|
}
|
|
.device-shell-frame {
|
|
position: absolute;
|
|
z-index: 0;
|
|
box-sizing: border-box;
|
|
inset: calc(var(--device-shell-inset) / -1);
|
|
border: var(--device-shell-border-width) solid var(--device-shell-stroke);
|
|
/* A saturating radius is resolved from the final border box. Using a
|
|
second fractional length here lets border-box and radius quantise on
|
|
different sides of a device pixel at some zoom/DPR combinations. */
|
|
border-radius: 9999px;
|
|
background: transparent;
|
|
box-shadow: var(--device-shell-shadow);
|
|
transition: border-color .15s, box-shadow .15s, opacity .2s;
|
|
pointer-events: auto;
|
|
/* Normative production fallback: never add a per-marker backdrop blur. */
|
|
backdrop-filter: none;
|
|
}
|
|
.device-shell:not(.with-values) {
|
|
left: 0;
|
|
top: 0;
|
|
}
|
|
.device-shell:not(.with-values):not(.text-shell) .device-shell-frame {
|
|
border-radius: 50%;
|
|
}
|
|
.device-shell.with-values.pos-right {
|
|
left: 0;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
.device-shell.with-values.pos-left {
|
|
right: 0;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
flex-direction: row-reverse;
|
|
}
|
|
.device-shell.with-values.pos-bottom {
|
|
left: 50%;
|
|
top: 0;
|
|
transform: translateX(-50%);
|
|
flex-direction: column;
|
|
}
|
|
.device-shell.with-values.pos-top {
|
|
left: 50%;
|
|
bottom: 0;
|
|
transform: translateX(-50%);
|
|
flex-direction: column-reverse;
|
|
}
|
|
.device-core {
|
|
position: relative;
|
|
z-index: 1;
|
|
box-sizing: border-box;
|
|
flex: 0 0 auto;
|
|
width: var(--dev-size);
|
|
height: var(--dev-size);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
background: var(--device-face-bg);
|
|
color: var(--device-face-fg);
|
|
box-shadow:
|
|
var(--device-core-inset-shadow),
|
|
0 0 0 var(--device-ring-width) var(--device-ring-color);
|
|
line-height: 0;
|
|
transition: background .15s, color .15s, box-shadow .15s, opacity .2s;
|
|
pointer-events: none;
|
|
}
|
|
.device-sections {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
justify-content: center;
|
|
gap: calc(var(--dev-size) * .08);
|
|
min-width: 0;
|
|
pointer-events: none;
|
|
}
|
|
.device-shell.pos-top .device-sections,
|
|
.device-shell.pos-bottom .device-sections {
|
|
flex-direction: row;
|
|
}
|
|
.dev ha-icon {
|
|
/* from --dev-size, NOT --icon-size: the per-device size multiplier must
|
|
scale the GLYPH with its badge. Pinned to the base size, "make this
|
|
icon bigger" grew an empty box around a default-size glyph (user
|
|
report via the owner, 2026-07-29). */
|
|
--mdc-icon-size: calc(var(--dev-size, var(--device-base-size, 2.25cqw)) * 0.55);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
line-height: 0;
|
|
}
|
|
.dev.on {
|
|
--device-face-bg: #F0A00C;
|
|
--device-face-fg: light-dark(#fff, #252525);
|
|
--device-shell-stroke: #F0A00C;
|
|
}
|
|
.dev.theme-light.on { --device-face-fg: #fff; }
|
|
.dev.theme-dark.on {
|
|
--device-face-fg: #252525;
|
|
--device-shell-stroke-ratio: .0375;
|
|
}
|
|
.dev.open {
|
|
--device-face-bg: var(--hp-open);
|
|
--device-face-fg: light-dark(#fff, #252525);
|
|
}
|
|
.dev.theme-light.open { --device-face-fg: #fff; }
|
|
.dev.theme-dark.open { --device-face-fg: #252525; }
|
|
.dev.lock-locked {
|
|
--device-face-bg: #66D17A;
|
|
--device-face-fg: light-dark(#fff, #252525);
|
|
--device-shell-stroke: #66D17A;
|
|
}
|
|
.dev.theme-light.lock-locked { --device-face-fg: #fff; }
|
|
.dev.theme-dark.lock-locked {
|
|
--device-face-fg: #252525;
|
|
--device-shell-stroke-ratio: .025;
|
|
}
|
|
.dev.lock-unlocked {
|
|
--device-face-bg: #F0410C;
|
|
--device-face-fg: light-dark(#fff, #252525);
|
|
--device-shell-stroke: #F0410C;
|
|
}
|
|
.dev.theme-light.lock-unlocked { --device-face-fg: #fff; }
|
|
.dev.theme-dark.lock-unlocked {
|
|
--device-face-fg: #252525;
|
|
--device-shell-stroke-ratio: .025;
|
|
}
|
|
/* Interaction wins ordinary state colours. Alarm keeps priority through
|
|
the more-specific rule below. Unavailable has no visual hover. */
|
|
:host([data-pointer-hover]) .dev:not(.unavail):hover {
|
|
--device-face-bg: #0C82F0;
|
|
--device-face-fg: light-dark(#fff, #252525);
|
|
--device-shell-stroke: var(--device-shell-base-stroke);
|
|
}
|
|
:host([data-pointer-hover]) .dev.theme-light:not(.unavail):hover { --device-face-fg: #fff; }
|
|
:host([data-pointer-hover]) .dev.theme-dark:not(.unavail):hover { --device-face-fg: #252525; }
|
|
:host([data-pointer-hover]) .dev:hover,
|
|
.dev:focus-visible { z-index: 5; }
|
|
/* #464: only markers that terminate a currently rendered local Zigbee
|
|
link rise above the topology overlay. The namespaced attribute is
|
|
transient and owned by hp-zigbee-topology-overlay. */
|
|
.dev[data-hp-zigbee-topology-endpoint] { z-index: 8; }
|
|
/* A real CSS hover is more specific than the base endpoint selector.
|
|
Repeat the endpoint contract at a still-higher specificity so the
|
|
hovered source cannot fall back below the topology overlay. */
|
|
:host([data-pointer-hover]) .dev[data-hp-zigbee-topology-endpoint]:hover { z-index: 8; }
|
|
.dev.unavail {
|
|
opacity: 0.35;
|
|
--device-face-bg: #B5BAC1;
|
|
--device-shell-stroke: var(--device-shell-base-stroke);
|
|
}
|
|
.dev.virtual .device-shell-frame {
|
|
border-style: dashed;
|
|
}
|
|
/* "hide from plan" flag, shown only in the device editor with the
|
|
"show hidden devices" toggle on (docs/FILTERING.md). BLUE, so a hidden
|
|
device cannot be mistaken for an unavailable one (translucent dark) —
|
|
and no live-state paint at all: a ghost is configuration, not status
|
|
(owner's request). */
|
|
.dev.ghost {
|
|
opacity: 0.6;
|
|
color: var(--hp-accent);
|
|
}
|
|
.dev.ghost .device-shell-frame {
|
|
border-style: dashed;
|
|
border-color: var(--hp-accent);
|
|
box-shadow: none;
|
|
}
|
|
.dev.ghost .device-core {
|
|
background: rgba(62, 166, 255, 0.22); /* fallback for old WebViews */
|
|
background: color-mix(in srgb, var(--hp-accent) 30%, var(--card-background-color, #1c2530));
|
|
color: var(--hp-accent);
|
|
}
|
|
/* HA-disabled is not a user hide: neutral grey + a power-off badge keeps
|
|
the two ghosts distinguishable without relying on colour alone. */
|
|
.dev.ghost.ha-disabled {
|
|
opacity: 0.62;
|
|
color: var(--secondary-text-color, #9aa0aa);
|
|
}
|
|
.dev.ghost.ha-disabled .device-shell-frame {
|
|
border-color: var(--secondary-text-color, #9aa0aa);
|
|
}
|
|
.dev.ghost.ha-disabled .device-core {
|
|
background: rgba(120, 124, 134, 0.2);
|
|
background: color-mix(in srgb, var(--secondary-text-color, #9aa0aa) 24%, var(--card-background-color, #1c2530));
|
|
color: var(--secondary-text-color, #9aa0aa);
|
|
}
|
|
.dev .habadge {
|
|
position: absolute;
|
|
right: -18%;
|
|
bottom: -18%;
|
|
display: grid;
|
|
place-items: center;
|
|
width: 46%;
|
|
height: 46%;
|
|
min-width: 12px;
|
|
min-height: 12px;
|
|
border-radius: 50%;
|
|
background: var(--card-background-color, #242832);
|
|
border: 1px solid currentColor;
|
|
}
|
|
.dev .habadge ha-icon { width: 72%; height: 72%; }
|
|
.dev.sel {
|
|
--device-ring-color: #F0A00C;
|
|
--device-ring-width: calc(var(--dev-size) * 0.0375);
|
|
}
|
|
.dev:focus-visible {
|
|
--device-ring-color: #0C82F0;
|
|
--device-ring-width: calc(var(--dev-size) * 0.0375);
|
|
}
|
|
.dev:not(.on):not(.open):not(.alarm):not(.lock-locked):not(.lock-unlocked):not(.unavail):focus-visible {
|
|
--device-face-fg: #0C82F0;
|
|
}
|
|
/* Alert stays above focus, selection, hover and ordinary semantic paint. */
|
|
.dev.alarm,
|
|
:host([data-pointer-hover]) .dev.alarm:hover,
|
|
.dev.alarm:focus-visible {
|
|
--device-face-bg: #F0410C;
|
|
--device-face-fg: light-dark(#fff, #252525);
|
|
--device-shell-stroke: #F0410C;
|
|
}
|
|
.dev.theme-light.alarm { --device-face-fg: #fff; }
|
|
.dev.theme-dark.alarm {
|
|
--device-face-fg: #252525;
|
|
--device-shell-stroke-ratio: .025;
|
|
}
|
|
.dev .value-badge {
|
|
position: relative;
|
|
z-index: 2;
|
|
box-sizing: border-box;
|
|
width: max-content;
|
|
min-width: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875);
|
|
height: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * .7875);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: visible;
|
|
background: var(--device-core-bg);
|
|
border: 0;
|
|
border-radius: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.39375);
|
|
padding: 0 calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.14);
|
|
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * var(--value-font-scale, .45));
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
color: var(--device-core-fg);
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
}
|
|
.dev .value-badge.unavailable,
|
|
.dev .value-badge.missing { opacity: 0.66; }
|
|
.dev .lqi {
|
|
position: absolute;
|
|
top: calc(50% + var(--device-shell-size) / 2 + var(--dev-size) * .05);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.05);
|
|
font-size: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.38);
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9);
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
}
|
|
.dev .lqi.below-value-badge {
|
|
margin-top: calc(var(--dev-size, var(--icon-size, 2.5cqw)) * 0.8875);
|
|
}
|
|
.temprange {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
margin-left: auto;
|
|
color: var(--hp-muted);
|
|
font-size: var(--fs-s);
|
|
}
|
|
/* #162: the dock says the moving robot is drawn nowhere. Amber is not the
|
|
only signal — the alert glyph carries the same meaning without colour,
|
|
and the accessible name carries the exact reason. */
|
|
.vacwarn {
|
|
position: absolute;
|
|
top: -12%;
|
|
right: -12%;
|
|
width: calc(var(--device-base-size, 2.25cqw) * 0.42);
|
|
height: calc(var(--device-base-size, 2.25cqw) * 0.42);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: light-dark(#fff, #252525);
|
|
border: 1px solid #E5A100;
|
|
color: #E5A100;
|
|
z-index: 7;
|
|
pointer-events: none;
|
|
}
|
|
.vacwarn ha-icon {
|
|
--mdc-icon-size: calc(var(--device-base-size, 2.25cqw) * 0.32);
|
|
color: inherit;
|
|
}
|
|
/* live vacuum: a round puck, no badge plate, soft pulse (docs/VACUUM.md) */
|
|
.vacpuck {
|
|
position: absolute;
|
|
/* the base badge, but round and 20% smaller — the owner's wording:
|
|
«иконка похожа на иконку базы, только круглая и чуть меньше» */
|
|
--puck-size: calc(var(--device-base-size, 2.25cqw) * 0.8);
|
|
width: var(--puck-size);
|
|
height: var(--puck-size);
|
|
border-radius: 50%;
|
|
transform: translate(-50%, -50%);
|
|
--vac-core-bg: light-dark(#fff, #252525);
|
|
--vac-core-fg: light-dark(#252525, #fff);
|
|
background: var(--vac-core-bg);
|
|
border: 1px solid #BCBCBC;
|
|
box-shadow:
|
|
0 1px 2px rgb(37 40 45 / 12%),
|
|
0 4px 8px -1.07px rgb(37 40 45 / 18%);
|
|
color: var(--vac-core-fg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
z-index: 6;
|
|
/* glide between sparse position updates; .jump disables it so the robot
|
|
never appears to drive through walls after a data gap */
|
|
transition: left 1.2s linear, top 1.2s linear;
|
|
animation: vacpulse 2.2s ease-out infinite;
|
|
}
|
|
.vacpuck.theme-light { --vac-core-bg: #fff; --vac-core-fg: #252525; }
|
|
.vacpuck.theme-dark { --vac-core-bg: #252525; --vac-core-fg: #fff; }
|
|
.vacpuck.jump { transition: none; }
|
|
.vacpuck.stale { opacity: 0.45; animation: none; }
|
|
.vacpuck ha-icon {
|
|
--mdc-icon-size: calc(var(--puck-size) * 0.68);
|
|
color: var(--vac-core-fg);
|
|
/* same centering recipe as .dev ha-icon: without flex + line-height 0
|
|
the glyph sits on its text baseline and appears to float around the
|
|
circle (owner report 2026-07-31) */
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
line-height: 0;
|
|
width: var(--mdc-icon-size);
|
|
height: var(--mdc-icon-size);
|
|
}
|
|
@keyframes vacpulse {
|
|
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--hp-accent) 45%, transparent); }
|
|
70% { box-shadow: 0 0 0 12px transparent; }
|
|
100% { box-shadow: 0 0 0 0 transparent; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.vacpuck { animation: none; }
|
|
}
|
|
.vactrail {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 5;
|
|
overflow: visible;
|
|
}
|
|
.vactrail polyline,
|
|
.vactrail path {
|
|
fill: none;
|
|
stroke-linejoin: round;
|
|
stroke-linecap: round;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
/* dark halo + light core: neutral, and one of the two always contrasts
|
|
with whatever fill is underneath */
|
|
.vactrail g.prev { opacity: 0.4; }
|
|
.vactrail .case {
|
|
stroke: rgba(0, 0, 0, 0.4);
|
|
stroke-width: 2.25;
|
|
}
|
|
.vactrail .core {
|
|
stroke: rgba(255, 255, 255, 0.82);
|
|
stroke-width: 0.9;
|
|
}
|
|
.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;
|
|
}
|
|
`;
|