{"slug": "handover-for-ha-dashboard-https-www-reddit-com-r-homeassistant-s-t5hpcf2vcx", "title": "Handover for HA dashboard https://www.reddit.com/r/homeassistant/s/t5HpCF2vcx", "summary": "A developer has documented a method for building hand-illustrated, animated floorplan dashboards for Home Assistant using the stock picture-elements card, with images that swap based on device states. The approach uses one image per floor, animated WebP files for device animations, and is designed for wall tablets or TVs, though it requires significant illustration work and manual placement.", "body_md": "**Who this is for:** an AI agent (or engineer) helping someone build an illustrated,\nanimated floorplan dashboard for Home Assistant **from scratch**, with their own\nhouse, their own illustrations, their own sensors.\n\n**How to read it:** it is written as questions and answers. Find the question the\nuser is really asking and answer from that section. The numbers, room names and\nentity IDs here are placeholders — every one of them will be different in a new\nbuild. What transfers is the *method*: the canvas rule, the file naming scheme,\nthe card patterns, and a long list of traps that cost real time to discover.\n\nNothing here requires the original artwork. A new build can have one floor or five, three devices or thirty.\n\nA hand-illustrated, isometric cutaway of a house, one image per floor, used as the home screen for Home Assistant. Devices are painted into the picture. When the air conditioner runs, the AC in the picture blows; when the TV is on, the screen glows and flickers; at night the whole house switches to a night render with warm windows. Small translucent \"pills\" sit over each room showing temperature, humidity and the AC setpoint.\n\nIt is built on Home Assistant's stock **`picture-elements`** card. The illustration\nis the card's background; every device is an absolutely-positioned `image` element\non top of it that swaps its own file when the entity changes state.\n\nOne Lovelace **view per floor or area**, each a full-screen panel holding a single\n`picture-elements` card. A reference build had four:\n\n| view | what it holds | \n|---|---|\n| Ground floor | floorplan + AC units, TV, wood stove + two room pills | \n| First floor | floorplan + AC units, monitor + four room pills | \n| Garden | three background states (day / night-dark / night-lit), tap zones on the lamps | \n| Combined | all three floors composited onto one canvas, sized for a TV | \n\nA new build can stop at one view. The combined view (Part 10) is genuinely optional and is the hardest part of the whole project.\n\n- **It is beautiful, and it is legible.** People who do not use Home Assistant can\nread it. \"The living room AC is on\" is a picture of an AC that is on.\n- **Almost entirely stock.** The core (backgrounds, day/night, animated devices,\ntap-to-open) needs no custom cards at all. Only optional extras pull in HACS.\n- **Animation costs nothing at runtime.** The motion lives inside the image files\n(animated WebP), not in CSS or JavaScript. The browser decodes an image. This is\nwhat makes it viable on a cheap wall tablet or a TV browser.\n- **It scales down gracefully.** Coordinates are percentages, so the card fills\nwhatever screen it lands on.\n- **Excellent for a wall display or TV.** Big, glanceable, no chrome, no scrolling.\n\n- **The artwork is the project.** Expect the illustration to be 80% of the cost and\nthe calendar time. If the user cannot commission or produce a consistent isometric\nset, stop here and suggest a photograph-based or SVG floorplan instead.\n- **Every device state is another drawing.** Adding a lamp later means going back to\nthe illustrator. This is the single biggest structural downside — see the file\ncount arithmetic in Part 4 before promising anything.\n- **Placement is manual.** The background does not contain the devices, so nothing\ncan auto-locate them. Each device is dragged into position by hand, once.\n- **`picture-elements` is a blunt tool.** No layout engine, no grouping, no boxes.\nEverything is absolutely positioned, so hiding an element leaves a hole. There is\none important exception (Part 9).\n- **It is not adaptive.** A phone gets the same picture, shrunk. If phone use matters,\nthe floorplan is a second dashboard, not the only one.\n- **Maintenance is YAML.** No GUI editing — the visual editor actively corrupts this\ncard (Part 11).\n- **Weight.** A full art set runs a few megabytes. Fine on a LAN, noticeable over a\nslow remote connection.\n\n- They have no illustration source and no budget for one.\n- They want every entity in the house on it. This design suits ~4–8 visible devices per view; past that it turns into noise and the file count explodes.\n- They mostly use their phone.\n- They want to add devices frequently. Each addition is an art request.\n\nA good compromise to offer: build **one** view for the main living space, with two or\nthree animated devices and the room pills. It is a fraction of the work and delivers\nmost of the impression.\n\n| requirement | why | \n|---|---|\n| Home Assistant with file access ( `/config` ) | the dashboard is YAML files on disk — Samba, SSH add-on, VS Code add-on, anything | \n| Willingness to run the dashboard in **YAML mode** | `!include` of shared element files is the whole architecture | \n| An illustration set, or a way to get one | see Part 3 | \n| A helper toggle for day/night | `input_boolean` | \n| Sensors that already work in HA | the dashboard displays entities, it does not create them | \n| An image tool that can encode animated WebP ( `img2webp` , from`libwebp` ) | plus `pngquant` for palette reduction | \n\nStart with **none**. Then, per feature:\n\n| feature | needs | avoidable? | \n|---|---|---|\n| backgrounds, day/night, animated devices, tap-to-open | nothing | — | \n| room status pills | nothing | — | \n| a popup controlling **two or more** entities from one tap | **Browser Mod** | yes — use a subview, or tap/hold for two dialogs | \n| the combined all-floors view | **card-mod** | no | \n| scaling a standalone view fluidly ( `--fp-u` ) | card-mod | yes, if only fixed-px sizing is wanted | \n\nTell the user plainly which line they cross and when. A stock-only build is a real option and worth defending.\n\nAnything reasonably current. Two version-sensitive points:\n\n- The modern condition schema (`condition: state` ,`condition: numeric_state` ,`condition: or` ) works in`picture-elements` conditionals from frontend**2024.2** onward. Older versions only take the legacy shorthand (`entity:` +`state:` ).\n- The `conditional` -as-flex-container behaviour in Part 9 was checked against release\ntags spanning several months, not just`dev` — the exemption is in the shipped\nbuilds, not a recent addition. Re-check if the target is much older.\n\nAsk, because it constrains CSS. A reference build targeted a smart-TV browser, which ruled out:\n\n- flexbox `gap` (Chromium 84+) — use margins on children instead\n- `100dvh` (Chromium 108+) — use`100vh`\n\nModern tablets and desktops have no such limits. If the target is a TV or an old tablet, keep to the conservative forms throughout; it costs nothing.\n\n**Every image must be exported on one shared canvas of the same size, with content\npositioned where it belongs in the house — not cropped to itself.**\n\nCall it the **master canvas**. Pick a size once (a reference build used 4096×2160 RGBA\nPNG), and have the illustrator render every single asset onto it: each floor's day\nrender, each floor's night render, and *every device layer*, all at their true position\ninside that canvas, transparent everywhere else.\n\nThree payoffs, all large:\n\n1. **The floors line up automatically.** Compositing all floors into one image later\nbecomes a plain alpha stack — no registration work, no guessing.\n2. **Device layers land themselves.** If a device is exported full-canvas, its position\nis already known; nobody has to drag it into place.\n3. **Coordinates survive.** Every position can be expressed in master-canvas pixels,\nwhich stay valid even if a crop box changes later. Converting percentage-to-percentage\nbetween two different crops is where mistakes happen — always go via master-canvas\npixels.\n\nIn the reference build the *backgrounds* were full-canvas but the *device layers* were\npre-cropped, so the devices had to be placed by hand with a drag tool. That is the\navoidable half. Ask for full-canvas device layers up front.\n\nPer view: find the content bounding box in the alpha channel, and **crop to it — never\nresize**. Source pixels then map 1:1 to the served image, so every measurement stays\nmeaningful.\n\nRecord, for each view, its crop origin and size. Those four numbers convert any master-canvas pixel to a card percentage:\n\n```\nleft% = (Xcentre − cropX) / cropW × 100\ntop%  = (Ycentre − cropY) / cropH × 100\n```\n\nStore them in a small `_meta.json` beside the built images. Future-you and future\ntools will need them.\n\n**The union of both.** Always.\n\nNight renders bleed outward — light spills from windows, so the opaque area extends further left and down. In the reference build the difference was 151 px left and 88 px down on one floor. Crop each render to its own box and the entire floorplan visibly jumps the moment the card swaps day for night.\n\nCrop day and night to the same union box. Both files then have identical dimensions and are pixel-aligned, and the swap is invisible. The same rule applies to any other whole-scene variant (a \"lights on\" night render, for example).\n\n1. One isometric cutaway per floor/area, **day** and**night** versions, on the master\ncanvas.\n2. For any whole-scene lighting state (garden lamps on, for instance): an additional full render.\n3. For every animated device: a **still \"off\" frame** and an**8-frame loop** for \"on\",\nin*both* day and night lighting, each on the master canvas.\n4. Device layers with their own drop shadows, on transparency.\n5. Everything as PNG with alpha. No flattening, no baked backgrounds behind device layers.\n\nYes, and enforce it. A reference build had one folder capitalised inconsistently\n(`FIreplace_Night` with a capital I) which broke every glob that touched it. Agree a\nscheme, lowercase it, and check before building.\n\nThis is the section to read before quoting the user a scope.\n\nOne per **whole-scene state**, which is the product of every global condition:\n\n| situation | files | \n|---|---|\n| day/night only | 2 — `<view>_day.webp` ,`<view>_night.webp` | \n| day/night + a scene light that changes the whole render | 3 — `<view>_day.webp` ,`<view>_night_off.webp` ,`<view>_night_on.webp` | \n\nNote the asymmetry in the second row: a garden's lamps are invisible in daylight, so day needs no on/off pair. Only draw the combinations that actually look different.\n\n**Warning:** this is a product, not a sum. Two independent global toggles = 4 renders\nper view; three = 8. Keep global states to one (day/night), plus at most one scene\nlight per view. Anything else should be a *device* overlay, not a background.\n\nFor a device drawn as its own layer with an on/off state, in a house that has day and\nnight backgrounds, it is **four files**:\n\n| file | when it shows | \n|---|---|\n| `<device>_off.webp` | day, entity off — a still | \n| `<device>_on.webp` | day, entity on — **animated** (or a still, if it does not move) | \n| `<device>_night_off.webp` | night, entity off — a still | \n| `<device>_night_on.webp` | night, entity on — **animated** | \n\nRules that go with those four:\n\n- The night art must be **geometrically identical** to the day art — same frame size,\nsame content position. That is what lets one set of`left` /`top` /`width` serve both.\nAssert it at build time.\n- **The off still and the on animation must share one canvas size.**`state_image` swaps the file but the element keeps one`width` and derives height from the image's\naspect ratio. Different sizes = the artwork jumps and resizes on every state change.\nThis bit the reference build exactly once, on a monitor whose glow spilled further in\nthe animation frames than in the still (189×284 vs 314×319).**Assert every frame\nequals the still before building a component.**\n- Naming: keep `_off` /`_on` for day and`_night_off` /`_night_on` for night. It is\nasymmetric and slightly ugly; it is also easy to glob and easy to read.\n\n`state_image` maps state → file, so any number of states is fine — you just need a\ndrawing for each visually distinct one. Two shapes come up:\n\n- **Many states, one appearance.** A climate entity has`cool` ,`heat` ,`heat_cool` ,`auto` ,`dry` ,`fan_only` and`off` . If the AC looks the same whenever it runs, map\nall six active modes to the*same*`_on` file. That is what the reference build does.\n- **A rare alternate look.** An occasional takeover (a special image on the TV screen,\nsay) is best gated by a separate`input_boolean` and drawn as extra`conditional` blocks, so it does not multiply the normal art. Same geometry, different file, so no\ncoordinates change.\n\nTwo different mechanisms; pick per light:\n\n1. **The light changes the whole scene** (a garden's lamps, a room whose glow floods the\nrender): make it a**background variant** , a full-canvas conditional overlay at`z-index: 0` . One extra render per state.\n2. **The light is a local object** (a lamp on a table): make it a**device layer** with`_off` /`_on` , exactly like any other device. Four files.\n\nOption 1 looks far better and costs one render; option 2 scales to many lights. Do not mix approaches for the same light.\n\nOne: a **1×1 fully transparent WebP** (about 34 bytes), per view folder. It is the\ncarrier for invisible tap targets — you place it, give it a width and height in CSS,\nand it becomes a clickable rectangle over any part of the picture (the lamp posts in a\ngarden, a whole room, a door). Stock `picture-elements` has no \"box\" element, so this\nis the standard workaround.\n\n```\nimages = views × background_variants\n       + animated_devices × 4\n       + views × 1            (the transparent hit-area carrier)\n```\n\nA three-view build with eight animated devices and one garden light:\n`(2 + 2 + 3) + 32 + 3` = **42 served files**, from roughly `8 devices × (2 stills + 16 frames)` = 144 source frames plus 7 background renders. Show the user that number\nbefore they commit.\n\n**WebP, throughout.** Every browser worth targeting renders it, including the browsers\nbuilt into smart TVs — those are Chromium-based and have been for many years. The\nwidely repeated claim that a given TV \"does not support WebP\" almost always refers to\nits native *media/photo viewer*, not its browser engine; test it with one sample image\nbefore believing it. A PNG-only build was tried in the reference project and reverted.\n\n| asset | recipe | \n|---|---|\n| Backgrounds | **lossy WebP, quality ~88.** Around 2.4/255 mean channel error on visible pixels — invisible in practice | \n| Device stills and animation frames | **`pngquant --quality 85-100` first, then encode LOSSLESS WebP** | \n\nThat second row is counter-intuitive and worth internalising: for flat, cel-shaded art\nwith a lot of transparency, **lossy WebP is both larger and worse than lossless**. One\nmeasured sprite sheet came out at 47 KB lossless-after-quantise versus 150 KB at q90.\nThe colour reduction is what does the work; the lossless encode then preserves it\nexactly. Do not \"optimise\" this by switching to lossy.\n\nException worth knowing: if a frame is *busy raster content* (a photographic image on a\nscreen, not flat cel art), palette reduction at 85-100 will fail to quantise and fall\nthrough to a full-colour lossless encode. Drop that one asset to `--quality 70-95`\nand check the error metric.\n\nA full floor's bundle — day and night backgrounds, every still, every animation — lands around 1 MB with this recipe.\n\n**Animated WebP.** The `_on.webp` file *is* the animation, looping forever. The card\nsimply swaps it in with `state_image`. No CSS animation, no JavaScript, no custom card.\n\nBuild with `img2webp`:\n\n```\nimg2webp -loop 0 -lossless -m 6 -d <ms_per_frame> \\\n    frame0.png frame1.png ... frame7.png -o <device>_on.webp\n```\n\n`-d` is **milliseconds per frame**, not a frame rate. `-m 6` is the slowest/best\nencoding method — if you are batch-encoding many files under a shell timeout, `-m 3`\nis a reasonable fallback.\n\nTune per device, on the actual target screen, and then leave them alone. Reference values, after tuning:\n\n| device kind | frames | per frame | loop | fps | \n|---|---|---|---|---|\n| air conditioner (fan blades) | 8 | 167 ms | 1.34 s | 6 | \n| fire / flame | 8 | 125 ms | 1.00 s | 8 | \n| TV / screen flicker | 8 → 6 encoded | 125/250 ms | 1.00 s | 8 | \n\nDo **not** normalise them to one speed, and do not try to make the loops the same\nlength — nothing depends on them being in sync, and matching them makes the picture\nlook mechanical. The AC was deliberately slowed from 8 to 6 fps after testing on the\nreal display.\n\nBecause if consecutive source frames are byte-identical after palette reduction,\n`img2webp` collapses them into one frame with a longer duration. Eight frames where\n2==3 and 6==7 becomes six frames with durations `[125,125,250,125,125,250]` — still\n1000 ms total, identical motion on screen. **This is not a bug; do not \"fix\" it.**\n\n**Parse the WebP container's `ANMF` chunks.** Do not trust an imaging library —\nPillow reports `duration: 0` on read even for correct files.\n\nAnd **use `img2webp`, not Pillow, to build them.** Pillow's `save_all` also merges\nidentical frames but *rewrites the durations* while doing it, which is easy to miss and\nchanges the loop length.\n\n- **`pngquant` silently flattens a multi-frame APNG to frame 1.** Quantise individual\nframes*before* assembling, never after.\n- **`img2webp` needs an explicit `-lossy` flag.** A bare`-q` is silently ignored and\nyou get a lossless file back.\n\nIt works — a sprite sheet with `steps(N)` and an animated `background-position-x` was\nbuilt and verified pixel-exact — but it **requires card-mod**, because `@keyframes` are\ntree-scoped: a rule defined in the document does not reach an element inside a shadow\nroot, and HA themes can only set CSS variables, not define rules. Since you are\ngenerating the assets anyway, the speed is baked in at encode time either way, so CSS\nbuys almost nothing for a HACS dependency. **Use animated WebP.**\n\n(If it ever is revived, the one rule that matters: the sheet must be **N+1 columns for\nN frames**. Percentage `background-position` resolves against *(container width −\nimage width)*, so with N+1 columns `steps(8)` lands exactly on frames 0..7. A plain\n8-column sheet lands on fractional frames and smears.)\n\nAs **YAML-mode Lovelace**: files on disk under `/config`, registered in\n`configuration.yaml`. Not through the UI.\n\n```\nlovelace:\n  mode: storage          # keeps existing UI-editable dashboards working\n  dashboards:\n    floor-plan-yaml:     # the slug MUST contain a hyphen\n      mode: yaml\n      filename: floorplan/dashboard.yaml\n      title: Floor Plan\n      icon: mdi:home-group\n      show_in_sidebar: true\n```\n\nAdding it needs **one restart**. After that, editing any of the files needs only a\nbrowser refresh — HA caches the dashboard on file mtime, so a newer file invalidates\nthe cache by itself. No restart, no reload service.\n\nUsing a new slug means it runs **side by side** with anything that already exists.\nAlways do that: confirm it works, then retire the old one.\n\n```\n/config/floorplan/\n    dashboard.yaml            the views; hand-edited\n    ground_elements.yaml      \\\n    upper_elements.yaml        }  the element lists, one per view; hand-edited\n    garden_elements.yaml      /\n    <anything>_elements.yaml     optional extra overlays\n\n/config/www/floorplan/\n    ground/  upper/  garden/     the served WebP, one folder per view\n```\n\n`/config/www/` is served at `/local/`, which is what every image path in the cards\nrefers to: `/local/floorplan/ground/ground_day.webp`.\n\nTwo reasons, and the first is the important one:\n\n1. **Single source of truth.** If a combined view is built later (Part 10), the same\nfile is`!include` d by*both* the standalone floor view and the combined view. A room\nis edited once and changes in both places. Verified: it is the same object in both.\n2. `dashboard.yaml` stays short enough to read.\n\n```\n- type: picture-elements\n  image: /local/floorplan/ground/ground_day.webp\n  elements: !include ground_elements.yaml\n```\n\nDo this from day one even if there is no combined view planned. It costs nothing.\n\nEach floor is a **panel view** holding one card:\n\n```\n- title: Ground Floor\n  path: ground-floor\n  icon: mdi:home-floor-g\n  type: panel\n  background: \"#1c1c1c\"\n  show_icon_and_title: true\n  cards:\n    - type: picture-elements\n      image: /local/floorplan/ground/ground_day.webp\n      elements: !include ground_elements.yaml\n```\n\nTo make the card fill the panel while keeping its aspect ratio, add card-mod CSS using that view's crop dimensions:\n\n```\n      card_mod:\n        style: |\n          ha-card {\n            width: min(100%, calc((100vh - 56px) * <cropW> / <cropH>));\n            margin: 0 auto;\n          }\n```\n\nUse `100vh` and a literal `56px` if a TV or older browser is a target; `100dvh` with\n`var(--header-height, 56px)` is nicer but needs Chromium 108+.\n\nThe core pattern — one stock `image` element per device:\n\n```\n- type: image\n  entity: climate.living_room_ac\n  image: /local/floorplan/ground/ac_living_off.webp        # the still\n  state_image:                                             # state -> animated file\n    cool:      /local/floorplan/ground/ac_living_on.webp\n    heat:      /local/floorplan/ground/ac_living_on.webp\n    heat_cool: /local/floorplan/ground/ac_living_on.webp\n    auto:      /local/floorplan/ground/ac_living_on.webp\n    dry:       /local/floorplan/ground/ac_living_on.webp\n    fan_only:  /local/floorplan/ground/ac_living_on.webp\n  tap_action:\n    action: more-info\n  style:\n    left: 31.80%\n    top: 78.88%\n    width: 12.824%\n    z-index: '2'\n```\n\nUnlisted states (`off`, `unavailable`) fall through to `image:`, so they handle\nthemselves.\n\nConventions that are not optional:\n\n- **Never set `height`** on an image element. It follows from the aspect ratio; setting\nit distorts the art.\n- `z-index` values are**strings** in YAML (`'2'` ), because CSS wants a string.\n- **Do not use YAML merge keys (`<<: *anchor`) inside element config.** These files are\nparsed server-side by Python, but the config reaches a frontend that uses js-yaml.\nPlain anchors/aliases (`&x` /`*x` ) are fine in`!include` d files — the frontend only\never sees expanded JSON — but merge keys are not worth the risk.\n\nDriven by one helper: `input_boolean.dashboard_night` (an automation on sun elevation\nsets it; a manual toggle is useful too).\n\nA `picture-elements` card's own `image:` **cannot be made conditional**. So:\n\n- the **day** background stays as the card's`image:`\n- the **night** background is painted as a`conditional` element at`z-index: '0'` ,`left: 50%` ,`top: 50%` ,`width: 100%` — covering the whole card\n\n```\n- type: conditional\n  conditions:\n    - entity: input_boolean.dashboard_night\n      state: 'on'\n  elements:\n    - type: image\n      image: /local/floorplan/ground/ground_night.webp\n      tap_action: {action: none}\n      style: {left: 50%, top: 50%, width: 100%, z-index: '0', pointer-events: none}\n```\n\nBecause both backgrounds are the same union crop (Part 3), they are pixel-aligned and nothing shifts.\n\n**Each device then needs two conditional blocks** — one for day, one for night — each\nholding one `image` element that uses `state_image` for its own on/off. Two dimensions,\ntwo mechanisms: `conditional` for day/night, `state_image` for entity state.\n`state_image` can only key off one entity, so it cannot carry both.\n\n`conditional` **ANDs** its conditions, so combinations are flat blocks, not nesting.\nDay/night × a mode toggle = four flat `conditional` blocks, each with both conditions\nlisted. Verbose, but completely predictable — and it keeps every block's geometry\nidentical so nothing can drift.\n\nPlace the 1×1 transparent WebP, give it explicit `width`, `height` and `display: block`:\n\n```\n- type: image\n  image: /local/floorplan/garden/ui_blank.webp\n  entity: light.garden_lamps\n  tap_action: {action: more-info}\n  style:\n    left: 32.37%\n    top: 65.55%\n    width: 70px\n    height: 70px\n    z-index: '8'\n    display: block\n    transform: translate(-50%, -20%)\n    cursor: pointer\n```\n\n`display: block` is explicit because a custom element defaults to `inline`, where\n`height` is ignored. The `transform` shifts the box off dead centre — useful for a lamp\nwhere the hit area should sit on the head and hang down the post.\n\nWhile positioning these, add a temporary `background: rgba(255,0,0,.3)` line to see the\nbox, then comment it out. Leave the commented line in the file for next time.\n\nWrite every pixel length as:\n\n```\nwidth: calc(70 * var(--fp-u, 1px))\n```\n\n`--fp-u` is a project-defined unit. A standalone view sets nothing, so the `1px`\nfallback applies and it behaves like plain pixels. A combined view (Part 10) sets\n`--fp-u: 0.0521vw` — exactly 1px at a 1920 viewport — and everything scales together.\nCosts nothing to adopt from the start and is painful to retrofit.\n\nLeave `border:` hairlines in real px so they cannot vanish sub-pixel.\n\nWrite a linter and run it after every edit. Three passes are enough to catch nearly everything:\n\n1. **YAML syntax** , with line and column. (Ruby's Psych parses HA's custom tags —`!include` ,`!secret` — without complaint, because it stops at the AST. Handy if\npyyaml is not available.)\n2. **Element structure** : every element has a`type` ; every`conditional` has both`conditions` and`elements` ; every`state_image` has an`entity` ; every`tap_action` /`hold_action` /`double_tap_action` names a real action.\n3. **Image references** : every`/local/floorplan/...` string resolves to a file that\nactually exists on disk. Strip cache-busting suffixes (`?v=2` ) first.\n\nOnly check nodes reached through an `elements:` list against the *element* vocabulary —\ncard configs (`views:`/` cards:`, and any popup content) use a different `type`\nvocabulary and will otherwise produce pages of false warnings.\n\nBy hand, with a drag tool. **There is no automatic way**, and it is worth knowing why:\nthe backgrounds do not contain the devices. A device layer is a separate drawing with\nits own drop shadow on transparency, so there is nothing in the background to match it\nagainst. Template matching was tried and lands on blank wall. Do not spend time on it.\n\nThe workaround is a **self-contained HTML page**: the background as an `<img>`, each\ndevice as a draggable `<img>` on top, and a readout of each one's `left` / `top` /\n`width` as percentages of the view crop. Generate it (embed the images as data URIs so\nit opens straight from disk with no server), drag, copy the numbers into the YAML.\n\nRegenerate the placer whenever the crop box or the device set changes.\n\n`left` and `top` are the **centre** of the element, as a percentage of the card.\n`width` is a percentage of the card width. Height is never set.\n\nTo convert a device's width in master-canvas pixels to a card percentage:\n\n```\nwidth% = frame_width_px / cropW × 100\n```\n\nA useful trick when art has a lit and an unlit render of the same scene: **difference\nthe two images and take the centroid of each bright blob.** The emitters are the only\ncompact bright deltas, so this locates every lamp exactly. It is how a reference build\nfound four lamp posts in a garden. Reusable any time there is an on/off pair.\n\nKeep a table of **master-canvas pixel centres** per device, alongside the percentages.\nIf the crop box ever changes, recompute percentages from the pixels. **Never convert\npercentage to percentage** between two different crops — that is where the errors come\nfrom.\n\nRough convention that works:\n\n| layer | z-index | \n|---|---|\n| night / scene background overlays | `'0'` | \n| device artwork | `'2'` –`'5'` , in painter's order (further from viewer = lower) | \n| invisible tap zones | `'8'` | \n| status pills and their contents | `'11'` | \n\nGive each device a distinct value in the order they should occlude each other.\n\n| card | where | stock? | \n|---|---|---|\n| **`picture-elements`** | every view — the whole dashboard | stock | \n| `vertical-stack` | only the combined view (Part 10), as an absolute-positioning host | stock | \n| `tile` (with`features:` ) | inside popups — big touch targets | stock | \n| `history-graph` | inside popups | stock | \n| `markdown` | inside popups, for templated detail | stock | \n| `heading` (with badges) | an alternative status strip above the floorplan | stock | \n| `card-mod` | the combined view; optional elsewhere | HACS | \n| `browser_mod` | popups that control more than one entity | HACS | \n\n| element | used for | \n|---|---|\n| `image` | every piece of device artwork, every background overlay, every tap zone | \n| `conditional` | day/night, per-state art, and — importantly — as a **flex container** (Part 9) | \n| `icon` | glyphs in the status pills | \n| `state-label` | sensor readouts in the status pills | \n| `state-icon` | avoid where possible — see Part 9 | \n\nGeneric shapes — substitute the user's real IDs, and **read them off their live\nsystem, never invent them**:\n\n| purpose | entity kind | example placeholder | \n|---|---|---|\n| day/night switch | `input_boolean` | `input_boolean.dashboard_night` | \n| air conditioning | `climate` | `climate.<room>_ac` | \n| room temperature | `sensor` (device_class temperature) | `sensor.<room>_temperature` | \n| room humidity | `sensor` (device_class humidity) | `sensor.<room>_humidity` | \n| air quality | `sensor` | `sensor.<room>_carbon_dioxide` | \n| TV / media | `media_player` | `media_player.<name>` | \n| lights | `light` | `light.<area>` | \n| a decorative/manual state with no real device | `input_boolean` | `input_boolean.dashboard_<thing>_on` | \n| a rare alternate artwork mode | `input_boolean` | `input_boolean.dashboard_<mode>` | \n\nThat last-but-one row matters: something drawn on the floorplan does not need a real\ndevice behind it. A wood stove with no sensor is perfectly well represented by an\n`input_boolean` the user flips when they light it.\n\n| domain | states to list in `state_image` | \n|---|---|\n| `climate` | every active mode: `cool` ,`heat` ,`heat_cool` ,`auto` ,`dry` ,`fan_only` | \n| `switch` ,`light` ,`input_boolean` | `\"on\"` (quote it — bare`on` is YAML`true` ) | \n| `media_player` | `playing` , and usually`on` too | \n\nEverything unlisted falls through to `image:`, which handles `off`, `unavailable` and\n`unknown` for free.\n\n`tap_action: {action: more-info}` — HA's own dialog for that entity. It is the right\nanswer nearly always: full controls, no design work, consistent everywhere.\n\nTwo exceptions:\n\n- **Purely decorative art** (a background overlay):`tap_action: {action: none}` plus`pointer-events: none` , so it never eats a tap meant for something beneath it.\n- **One tap needs to control two or more entities.**`more-info` takes exactly*one* entity. Options, in the order worth offering:\n  1. A **subview** with a normal card — fully stock, but it is a page change (awkward\nwith a TV remote).\n  2. A **Browser Mod popup** — best UX, first HACS dependency.\n  3. Tap and hold for two different dialogs — stock, but undiscoverable.\n  4. A group helper — its dialog leads with the group toggle, usually the wrong shape.\n- A \n\nBrowser Mod 2 style — `fire-dom-event`, **not** `call-service` / `perform-action`:\n\n```\ntap_action:\n  action: fire-dom-event\n  browser_mod:\n    service: browser_mod.popup\n    data:\n      title: Garden watering\n      dismissable: true\n      timeout: 120000          # auto-close if left open\n      initial_style: wide\n      content:\n        type: vertical-stack\n        cards:\n          - type: tile\n            entity: valve.<something>\n            features: [{type: toggle}]\n          - type: history-graph\n            hours_to_show: 24\n            entities: [{entity: sensor.<something>}]\n      right_button: Start now\n      right_button_variant: success\n      right_button_close: true\n      right_button_action:\n        action: perform-action\n        perform_action: switch.turn_on\n        target: {entity_id: switch.<something>}\n      left_button: Close\n      left_button_close: true\n```\n\nPut the same popup on every element that should open it (a `conditional` element\n**cannot carry a `tap_action` itself**, so it goes on the icons inside). If the block\nis repeated many times, a plain YAML anchor deduplicates it safely — these files are\nresolved server-side.\n\nYes, and it is a legitimate alternative design to the in-picture pills:\n\n- A **`heading` card** directly above the floorplan, whose badges lay out in a real\nhorizontal row. Each badge takes a`visibility:` block, and a badge whose condition\nis false is removed and the row closes up. (Badge`visibility:` works but is\nundocumented.) The view must be`type: sections` , not`panel` , for this.\n- **View-level badges** — the only ones that expose a size variable (`--ha-badge-size` ,\ndefault 36px).\n\nThe size difference is worth knowing: **heading-badge icons are hard-coded at 14px**\nwith no `var()`, so no theme can enlarge them; only card-mod can. If something large is\nwanted, use view-level badges. If in-picture pills are wanted, read Part 9.\n\nYes — with one specific trick, and it is the most useful discovery in this whole project.\n\nThe general rule is that it *cannot*: every element the card renders gets a `.element`\nclass carrying `position: absolute; transform: translate(-50%,-50%)`, so elements never\naffect one another. Hide one and you get a hole.\n\n**But `conditional` elements are deliberately exempted from that class.** The frontend\nexcludes `HUI-CONDITIONAL-ELEMENT` on purpose, to keep it a transparent, static\ncontainer that appends children into itself. And `style:` is applied as **inline**\nstyles to every element, the conditional included — and inline beats a class.\n\nTherefore: style a `conditional` as an absolutely-positioned `inline-flex` box, and set\n`position: static; transform: none` on each of its children. They flow left to right,\nthe container auto-sizes, and a slot that vanishes takes its width with it.\n\n```\n- type: conditional\n  conditions: []              # always true: checkConditionsMet runs conditions.every()\n  style:\n    position: absolute\n    left: calc(29.5% - 12 * var(--fp-u, 1px))\n    top: calc(66.28% + 0px)\n    transform: translate(0, -50%)\n    display: inline-flex\n    align-items: center\n    height: calc(34 * var(--fp-u, 1px))\n    padding: 0 calc(11 * var(--fp-u, 1px))\n    box-sizing: border-box\n    background: rgba(60, 64, 72, 0.35)\n    border-radius: calc(17 * var(--fp-u, 1px))\n    backdrop-filter: blur(calc(3 * var(--fp-u, 1px)))\n    border: 1px solid rgba(255, 255, 255, 0.14)\n    z-index: '11'\n    color: '#bbbbbb'                                 # inherited by every slot\n    '--mdc-icon-size': calc(22 * var(--fp-u, 1px))   # ditto\n    pointer-events: none\n  elements:\n    - type: icon\n      icon: mdi:thermometer\n      tap_action: {action: more-info, entity: sensor.<room>_temperature}\n      style:\n        position: static\n        transform: none\n        display: flex\n        align-items: center\n        pointer-events: auto\n    - type: state-label\n      entity: sensor.<room>_temperature\n      style:\n        position: static\n        transform: none\n        margin-left: calc(-2 * var(--fp-u, 1px))\n        color: '#ffffff'\n        font-size: calc(15 * var(--fp-u, 1px))\n        font-weight: '600'\n        white-space: nowrap\n        line-height: '1'\n```\n\nThe container **is** the pill — it sizes itself, so there is no separate background\nelement and no second width for the \"something is hidden\" case.\n\n- **Spacing goes on each slot's LEADING element, never as flex `gap` on the container.** A`conditional` element whose conditions fail does*not* get`display: none` (that is\nthe conditional**card** , a different component). It merely empties itself and stays\nin the DOM as a**zero-width flex item** . A`gap` would still be painted around it and\nleave exactly the hole this design removes. (`gap` is also Chromium 84+, unsafe on a\nTV.)\n- **Use `type: icon`, not `type: state-icon`, for slot glyphs.**` state-icon` renders a`state-badge` whose shadow root hard-codes`width: 40px; height: 40px` on`:host` with no`var()` — unreachable, and in flow layout it sets the slot width.`type: icon` renders a bare`ha-icon` sized by`--mdc-icon-size` .`tap_action: {action: more-info, entity: X}` still works on it, because the action\nhandler checks`actionConfig.entity` before`config.entity` .\n- **`pointer-events: none` on the container, `auto` on the interactive children.**\n- Anything that can come and go goes in a **nested**`conditional` with`display: inline-flex; align-items: center` , carrying its margin on the element*inside* it.\n\nBecause `align-items: center` centres each child's **box**, and an icon's box is not\nits glyph. The icon host is `display: block` with an inherited line-height, so its line\nbox carries a baseline strut: a 22px glyph sits at the top of a ~29px box with the\ndescender space hanging below. Centring the box puts the glyph ~3.5px high.\n\nFix, on every flow child:\n\n```\n    - type: icon\n      style:\n        display: flex           # collapse the host box onto the glyph\n        align-items: center\n    - type: state-label\n      style:\n        line-height: '1'        # 40px box -> 31px, stops overflowing the pill\n```\n\n**Measure this against the running dashboard, not a mock.** A hand-built HTML mock with\n`<span>` placeholders has no line-height strut and will report everything as perfect.\nA mock is reliable for widths, reflow and collapse-to-zero — all container behaviour —\nand blind to anything that depends on what a real HA element puts in its own box.\n\n**10u between slots, −2u between an icon and its own value** (where `u` is\n`var(--fp-u, 1px)`).\n\nThe negative is not a mistake. An MDI glyph does not fill its `--mdc-icon-size` box —\nit carries several px of side bearing inside the SVG, so the gap you *see* is the CSS\nmargin plus that bearing. Going from 4u to 2u looks like almost no change for exactly\nthat reason. −2u eats into the bearing and lands the value where it looks right.\n\nDo not push much further: bearing varies per glyph, so a large negative that looks fine on one slot crowds another. −4u is about the limit. A first pass at 14u between slots read as loose; check on the real display before raising it.\n\nBecause `state-label` renders:\n\n```\nprefix + (config.attribute ? stateObj.attributes[config.attribute]\n                           : hass.formatEntityState(stateObj)) + suffix\n```\n\nOnly the **no-attribute** branch goes through `formatEntityState`, which is what appends\nthe unit. Read an attribute (`attribute: temperature` on a climate entity) and you get\nthe bare JS value — `22`, not `22 °C`.\n\nFix with the element's own `suffix: ' °C'`. Consequence: the suffix is hard-coded and\ndoes **not** follow HA's unit system, so an instance that switches to imperial needs\nthose labels edited.\n\nBadges behave differently and are not a counter-example — `state_content:` runs through\nthe attribute-formatting pipeline and appends the unit itself. Same data, different\nrenderer. Never assume behaviour transfers between badges and picture-elements.\n\nIf you used `type: state-icon`, it renders a `<state-badge>` whose shadow root sets, on\nits own `:host`:\n\n```\n:host { color: var(--paper-item-icon-color, #44739e); }\n```\n\nThat is an explicit declaration **on the element itself**, so it beats any `color`\ninherited from the wrapper. The glyph stays HA's default blue-grey and your `color:`\nlooks completely ignored.\n\nCustom properties *do* cross shadow boundaries, so set the property instead:\n\n| wrapper sets | resulting glyph colour | \n|---|---|\n| `color: #fff` | `#44739e` — ignored | \n| `--paper-item-icon-color: #fff` | white | \n\nSet `--paper-item-icon-color`, `--state-icon-color` and `color` together, plus\n`state_color: false` so HA does not recolour by entity state. (With `type: icon` this\ndoes not arise — plain `color:` works.)\n\n**The general rule:** when something inside an HA component ignores your style, check\nwhether the component exposes a custom property for it. If it does, set the property.\nIf the value is a bare literal with no `var()`, only card-mod can win.\n\n**One `%` anchor per room, offsets from it in scalable units.** The anchor tracks the\nartwork as the card resizes; the contents keep their shape and stay legible instead of\nshrinking. To move a whole panel, change its two anchor numbers and nothing else.\n\nKeep panels **horizontal** — one row: `🌡 21.4 °C  💧 48 %  ❄ 22 °C`. A vertical\nstack of icon/value pairs was tried and rejected.\n\nSensors and a mode glyph. Not toggles — tapping any glyph opens `more-info`, which has\nthe controls. A workable set per room: temperature, humidity, optionally air quality,\nand the climate mode icon plus setpoint.\n\nFor climate, **use `hvac_mode` (the entity state), not `hvac_action`.** `hvac_action`\ntracks what the compressor is doing moment to moment, so the icon flickers between\ncooling and idle as the unit cycles. `hvac_mode` is what the user *set*, so it stays\nput — and being the entity state rather than an attribute, the conditions are plain\n`entity` + `state`.\n\nCore ships no per-mode climate icons, so pick a vocabulary and reuse it:\n`cool` ❄ `mdi:snowflake` · `heat` 🔥 `mdi:fire` · `heat_cool` `mdi:sun-snowflake-variant`\n· `auto` `mdi:thermostat-auto` · `dry` `mdi:water-percent` · `fan_only` `mdi:fan` ·\n`off` `mdi:fan-off`.\n\nThe setpoint slot should be conditional on `state_not: 'off'` — it is the slot that\ncomes and goes, and with the auto-fit container its absence simply shrinks the pill.\n\nSkip this section unless the user specifically wants every floor on one screen. It is the most complex part of the project and it needs card-mod.\n\n**No.** `picture-elements` cannot host cards. Verified in-browser: `hui-element` is not\na registered custom element (\"Custom element doesn't exist\"), and `hui-card` is\nregistered but has no `setConfig`, which is what the element factory calls\n(\"r.setConfig is not a function\"). Do not retry that route.\n\nA **`vertical-stack`** whose children are absolutely positioned by card-mod over a\ntransparent spacer:\n\n- **Child 1** is a fully transparent PNG the exact size of the union of all floors,\nleft in normal flow. It gives the stack's`#root` the right aspect ratio, so the\npercentage`top:` values below have a height to resolve against.\n- **Children 2..N** are the floor cards, absolutely positioned on top, each with a`z-index` .\n\n```\n- type: vertical-stack\n  card_mod:\n    style: |\n      #root { position: relative; overflow: hidden; }\n      #root > hui-card:nth-child(2) { position: absolute; left: 0%;      top: 31.26%; width: 57.84%; z-index: 2; }\n      #root > hui-card:nth-child(3) { position: absolute; left: 3.97%;   top: -8.96%; width: 48.24%; z-index: 3; }\n      #root > hui-card:nth-child(4) { position: absolute; left: 44.48%;  top: 18.90%; width: 55.52%; z-index: 1; }\n  cards:\n    - ...\n```\n\nEach nested card gets card-mod making it transparent and click-through:\n\n```\n      card_mod:\n        style: |\n          ha-card {\n            --fp-u: 0.0521vw;\n            --ha-card-background: transparent;\n            --ha-card-border-width: 0;\n            --ha-card-box-shadow: none;\n            pointer-events: none;\n          }\n          ha-card * { pointer-events: auto; }\n```\n\n`pointer-events: none` on the card with `auto` on its children lets clicks fall through\nthe transparent regions to the floor beneath.\n\nThe **plain string form** (`card_mod: style: |`), which injects into the stack's own\nshadow root where `#root` matches. The `hui-vertical-stack-card $:` key form does\n**not** work — tested, no effect.\n\n`gap: 0` is unnecessary: absolutely positioned children leave flex layout, so with one\nin-flow spacer there are no gaps.\n\nFrom three constants, in a script — never by hand, and never let the spacer image and the percentages be edited independently:\n\n| constant | meaning | \n|---|---|\n| `CANVAS_BOX` | each view's crop box on the master canvas | \n| `UPPER_LIFT` | how far to raise an upper floor for an exploded look | \n| `TOP_CROP` | dead space to trim off the top of the union | \n\nThe script derives the union canvas, every card placement percentage, the stack CSS and\nthe transparent spacer image, so the image and the config can never disagree. Reference\nthis back to the spacer through a **self-versioning URL** (`?v=<width>x<height>`) so a\nstale copy can never be served — and use the *same* URL on every card that references\nit, or an overlay will position against the wrong height.\n\nHave the script **patch only the geometry lines in place and print a diff**. It must\nnever rewrite the file wholesale (Part 11).\n\n**Lower floors go on top of outdoor areas; upper floors go on top of lower floors.**\nA garden belongs at the *bottom* — put it on top and its furniture draws straight\nacross the house interior.\n\nJudge overlap by **counting opaque pixels in the intersection**, never by a whole-image\nmean pixel difference. A mean over the full frame averages large localised differences\naway — that is precisely how a reference build once concluded the z-order \"barely\nmattered\" and stacked a garden over a house.\n\nEnough for the exploded look, not enough to shrink everything. Zero overlap in the reference build needed 685px and cost another 8% of display width; 435px left 30% of the original overlap, confined to a rear wall and roof, which reads naturally. There is a real trade-off: the taller the union, the more a 16:9 screen fits it by height and the smaller everything renders. Build a drag tool that shows live overlap, the resulting union aspect, and the cost in display width, and let the user choose.\n\nAdd another `picture-elements` card to the stack, using the same transparent spacer as\nits image, with its own `!include` d elements file, positioned at `left: 0; top: 0; width: 100%` and a high `z-index`.\n\n**Its `pointer-events: none` must be cleared on the HOST, in the stack's own CSS**, not\njust inside the card:\n\n```\n#root > hui-card:nth-child(5) { position: absolute; left: 0; top: 0; width: 100%; z-index: 4; pointer-events: none; }\n```\n\ncard-mod can only reach inside a card's shadow root, so `ha-card` rules leave the\n`hui-card` host itself hittable and the overlay still swallows every click on the whole\ndashboard.\n\nAnd inside the overlay card, do **not** write a blanket `ha-card * { pointer-events: auto }` — the container div spans the whole canvas, so that makes the entire dashboard\nunclickable. Set `ha-card div, ha-card hui-image { pointer-events: none; }` and have\neach genuinely interactive element opt back in with its own inline\n`pointer-events: auto`.\n\nPlace such an overlay in an area no floor card occupies. Work out where that is from the geometry, do not guess.\n\nThe **modern** one — `condition: state`, `condition: numeric_state`, `condition: or` —\non frontend ≥ 2024.2. `hui-conditional-element` and `hui-conditional-card` share\n`HuiConditionalBase`, so the picture-elements element gets the same `checkConditionsMet`\nthe card does. Both schemas can coexist in one file, because `checkConditionsMet` falls\nthrough to a state check for any condition with no `condition:` key.\n\nTwo traps if you use `numeric_state`:\n\n- **`below` is EXCLUSIVE.** With an integer 0–10 sensor, a red block at`above: 8` and\nan amber guard at`below: 8` leaves the value 8 matching*neither* — no badge at all,\nat the one value most likely to be sitting there. Use`below: 9` .\n- **`numeric_state` is FALSE for a non-numeric state.** An`unavailable` sensor makes\nevery`numeric_state` on it false,*including a negated guard* . Add a`state: [unavailable, unknown]` escape hatch OR'd beside it.\n\nIf two badges are mutually exclusive, make them **actually mutually exclusive** rather\nthan stacking them by z-index. Two identical glyphs layered will leak the lower one\nthrough the upper one's antialiased edges and double the drop shadow.\n\n**Regenerating a file wholesale from a generator and overwriting it.** In the reference\nproject this destroyed real work twice: once clobbering hand-entered entity IDs, once\ndiscarding hand edits to a deployed dashboard file with no backup.\n\nThe rule: **read what is on disk, make the targeted edit, diff, then write.** Assume\nevery file may have been edited by the user outside your session since it was last\ngenerated. If a generator is involved, have it *patch specific lines in place* and print\na diff of exactly what it touched.\n\nRun `git status` before any generator. If a file is already dirty, read the diff first —\nthat is how a hand edit gets silently reverted.\n\nAlso: never read a file back *after* an overwrite and report that nothing was lost. That\ncompounds the error into a false all-clear.\n\nDecide this explicitly, write it down at the top of the repo, and keep it true:\n\n| file | status | \n|---|---|\n| `*_elements.yaml` | **SOURCE** — hand-edited, nothing generates them | \n| `dashboard.yaml` | **SOURCE** — hand-edited; a script may patch only its geometry lines | \n| the transparent spacer PNG | derived | \n| the drag placer HTML | derived | \n\nThe one thing worse than not having this table is having one that is out of date.\n\n**Three attempts, maximum.** Then stop, and tell the user what the check says, what you\nchanged, and what you think is actually wrong. A fourth guess is worth less than five\nseconds of the user looking at it.\n\n**Never commit or deploy while a check is failing** — not the broken file, not \"the rest\nof the work\" alongside it, not with `--no-verify`.\n\n**No — the visual editor corrupts them.** HA's image picker rewrites a plain `image:`\nstring into an object:\n\n```\nimage:\n  media_content_id: /local/floorplan/ground/ground_day.webp\n  media_content_type: ''\n  metadata: {...}\n```\n\n`hui-image` expects a **string** and sets `<img src>` from it directly, so an object\nrenders nothing — silently, with no console error and no log line. Seen in the wild on\na background and on two device layers after one GUI edit. The GUI editor also strips all\ncomments.\n\nKeep `image:` and every value inside `state_image:` a plain `/local/...` string, and use\nthe **raw configuration editor**, never the per-card visual editor.\n\nCheck the browser console for `themesReady: Timeout waiting for themes to become ready`.\ncard-mod 4.x gates all styling behind themes being loaded, and on an instance with **zero\ncustom themes defined** it can time out and silently do nothing. Defining any theme at\nall resolves it.\n\nYou cropped day and night to their own bounding boxes instead of the union. See Part 3.\n\nThe off still and the on animation have different canvas sizes. See Part 4.\n\nIf it has already happened and re-exporting is not an option: recover the offset by\nmasked template matching of the still into frame 0 (`cv2.matchTemplate` with `TM_SQDIFF`\nand the alpha as mask; cross-check with an alpha-silhouette `TM_CCORR_NORMED` — both\nshould agree), then pad the still onto the animation's canvas at that offset. Then\nrecompute the element so nothing moves on screen: new canvas top-left = old still\ntop-left − (dx,dy); new centre = that + canvas/2. Verify the padded still lands\npixel-identically to the old one.\n\n- **Deploy the files you mean, not the whole directory.** The copies on the HA box can\ncarry hand edits. Wrap`scp` in a script that refuses anything outside the intended\nsource folder and destination, and offer a`--dry-run` .\n- If the deploy target runs OpenSSH ≥ 9, `scp` speaks SFTP and does**not** shell-expand\nthe remote path. Quoting it puts literal quotes in the filename and produces`remote mkdir \"'/config/floorplan/'\": No such file or directory` — which reads like a\nconnectivity error and is not one. Pass the remote path unquoted.\n- Deploying does not lint. A failing check means do not push that file.\n\nTwo asymmetries worth knowing:\n\n- **CSS custom properties DO cross shadow boundaries** , so a plain HA theme can restyle\ntext inside every card with no HACS.\n- **`@keyframes` do NOT cross** — they are tree-scoped, so a rule defined in the\ndocument never reaches an element inside a shadow root. That asymmetry is exactly why\na CSS-animation approach needs card-mod and an image-based one does not.\n\nTheme variables are written **without** the leading `--` in a theme file.\n\nUse these to size the job. The answers change the artwork brief, which is the expensive part.\n\n1. **How many views?** One area, one floor, the whole house?\n2. **Which devices should be visible and animated?** Get a list. Four to eight per view.\n3. **Which of those are real entities, and which are just things they want to see?** (The second kind gets an`input_boolean` .)\n4. **Day/night?** Doubles the art. Almost always worth it, but it is their call.\n5. **Any whole-scene lighting state** beyond day/night?\n6. **What screen is this for?** TV, wall tablet, desktop, phone. This decides the CSS\nconservatism and the aspect ratio.\n7. **Do they want all floors on one screen?** If yes, Part 10, and it needs card-mod.\n8. **Are they willing to install HACS components?** Establish the line early.\n9. **Where does the artwork come from?** If there is no answer here, stop and solve\nthat first.\n10. **Which sensors already exist in HA?** Read the real entity IDs from their system.\nNever invent them, never guess a naming pattern.\n\n1. **Settle the artwork brief.** Master canvas size, view list, device list, states per\ndevice, day/night. Give the illustrator the full-canvas rule in writing.\n2. **Get one view's art only** — one floor, day and night, one device. Prove the whole\npipeline end to end before commissioning the rest.\n3. **Crop to unions, record crop boxes** in a`_meta.json` per view.\n4. **Encode** : backgrounds lossy q88; stills and frames pngquant-then-lossless; build\nthe animated WebP with`img2webp` and verify the`ANMF` durations.\n5. **Copy to `/config/www/floorplan/<view>/`** , register the YAML dashboard, restart\nonce. Confirm the background alone renders.\n6. **Build the drag placer** for that view; place the devices; paste the percentages.\n7. **Add day/night** : the helper, the conditional background overlay, then two\nconditional blocks per device.\n8. **Add tap actions** (`more-info` almost everywhere).\n9. **Add one status panel** , using the auto-fit container recipe. Measure the icon\nalignment**on the running dashboard** , not in a mock.\n10. **Show the user.** Iterate on spacing and animation speed on the*real* display.\n11. **Repeat for the remaining views.**\n12. **Only then** , if wanted, build the combined view.\n13. **Write a linter** and run it on every edit. Write down what is source and what is\nderived.\n\n- The master canvas size, and each view's crop box and origin.\n- Each device's centre in **master-canvas pixels** , not only percentages.\n- The frame count and per-frame duration for every animation, and why it was chosen.\n- The real entity IDs, marked as read from the live system.\n- Every trap you hit, with the symptom that led to it. The symptom is the part that makes the note findable later — \"the icons are glued to the top of the pill\" is worth more than \"line-height strut affects flex centring\".\n\n*End of handover. If something in here contradicts what the running dashboard actually\ndoes, the running dashboard is right — go and measure it.*", "url": "https://wpnews.pro/news/handover-for-ha-dashboard-https-www-reddit-com-r-homeassistant-s-t5hpcf2vcx", "canonical_source": "https://gist.github.com/JohnJocoo/4160edc194181d359a524d7cca17a1e0", "published_at": "2026-09-08 19:51:23+00:00", "updated_at": "2026-09-09 07:57:17.291286+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Home Assistant"], "alternates": {"html": "https://wpnews.pro/news/handover-for-ha-dashboard-https-www-reddit-com-r-homeassistant-s-t5hpcf2vcx", "markdown": "https://wpnews.pro/news/handover-for-ha-dashboard-https-www-reddit-com-r-homeassistant-s-t5hpcf2vcx.md", "text": "https://wpnews.pro/news/handover-for-ha-dashboard-https-www-reddit-com-r-homeassistant-s-t5hpcf2vcx.txt", "jsonld": "https://wpnews.pro/news/handover-for-ha-dashboard-https-www-reddit-com-r-homeassistant-s-t5hpcf2vcx.jsonld"}}