-
Humidity
-
${humidity}%
+
+
+
+
+ ${city}
+
+
${typeof temp === 'number' ? Math.round(temp) : temp}°
+
${conditionLabel}
+
+
+
+ ${typeof humidity === 'number' ? Math.round(humidity) : humidity}%
+
+
+ ${typeof wind === 'number' ? Math.round(wind) : wind} ${windUnit}
+
+
-
-
Wind
-
${wind} km/h
+
+
+
+
${typeof temp === 'number' ? Math.round(temp) : temp}°C
+
+ ${hasForecast ? `
${forecastHtml}
` : ''}
`;
this.shadowRoot.querySelector('.card').addEventListener('click', () => this._handleClick());
}
@@ -90,5 +188,5 @@ class LumaWeatherCard extends HTMLElement {
customElements.define('luma-weather-card', LumaWeatherCard);
window.customCards = window.customCards || [];
-window.customCards.push({ type: 'luma-weather-card', name: 'Luma Weather', description: 'Luma-style weather hero card with temperature and conditions.', preview: true });
+window.customCards.push({ type: 'luma-weather-card', name: 'Luma Weather', description: 'Luma-style weather card with city, forecast, and conditions — matches TV layout.', preview: true });
console.info(`%c LUMA-WEATHER-CARD %c v${LUMA_WEATHER_VERSION} `, 'background:#f0d080;color:#111;font-weight:bold', 'background:#161a22;color:#f0d080');
diff --git a/design-system/CLAUDE.md b/design-system/CLAUDE.md
new file mode 100644
index 0000000..49ebd0d
--- /dev/null
+++ b/design-system/CLAUDE.md
@@ -0,0 +1,15 @@
+# Design system — instructions for Claude Code
+
+This folder is the design source for the Home Assistant tab. When you change any HA tab UI
+(cards, panels, focus, remote handling, hint bar):
+
+1. Read `ux-mechanics.md` Part A, then the section for the card you are touching. Implement
+ the numbers exactly (0.4 s hold, act on release, 200 ms send debounce, steps, clamps).
+2. Take sizes, colours and timings from `tokens.json`; don't invent new constants.
+3. Match the layout in `components.md` and the matching render in `screens/`. When you can
+ render a desktop screenshot (`--script … --screenshot`), compare it against that PNG.
+4. Work one card at a time, and say which rules you could not verify without the TV.
+5. Room card photos are provided by Home Assistant (each area's picture), fetched and cached at runtime
+ as `ux-mechanics.md` §B11 describes. Never add room photos to the repo, the IPK or Luma's settings.
+6. If the code and this folder disagree, follow this folder and point out the difference.
+ Don't edit files here unless asked — they're exported from the design canvas.
diff --git a/design-system/README.md b/design-system/README.md
new file mode 100644
index 0000000..c4338e8
--- /dev/null
+++ b/design-system/README.md
@@ -0,0 +1,58 @@
+# Luma Home design system
+
+The design for the Home Assistant tab of Luma: a 10-foot UI driven only by the remote's
+arrows, OK and Back, viewed from 5–6 m on a 65" TV.
+
+Exported 2026-10-27 from the "Luma — 10-foot redesign" design canvas and the "Luma Home"
+design system on claude.ai. Those stay the place to change the design; re-export this folder
+after changes.
+
+## Read in this order
+
+| File | What it is | When to use it |
+|---|---|---|
+| `ux-mechanics.md` | **The behaviour contract.** Global rules (tap vs hold, focus, grid, panels, errors) and every card's keys, panel, states, hint bar and HA calls. | Before writing any input or state logic. Numbers here are the spec. |
+| `components.md` | Layout and look of the shell, each card type and each panel. | When drawing a card or panel. |
+| `tokens.json` | Colours, type sizes, spacing, radii, timings — at logical 1920 × 1080. | Source of every constant. Don't hard-code values that exist here. |
+| `tokens.css` | The same tokens as CSS variables. | Lovelace cards in `cards/` and web previews. |
+| `screens/` | PNG renders of every design board. | Visual reference; compare desktop screenshots against these. |
+| `icons/` | 58 single-colour SVG icons + name map and attribution. | Compile to `.lvec` like other vector assets. |
+| `assets/roborock-s7.png` | Transparent vacuum photo for the vacuum card. | Vacuum card middle band. |
+
+**Room photos come from Home Assistant, not from this folder or the app.** The room card shows the picture set on each Home Assistant area, loaded over the existing HA connection (rules: `ux-mechanics.md` §B11, *Room photos*). The blurred room photos in `screens/` are mock-up placeholders: never add them, or any room photo, to the repository or the IPK.
+
+## Screens
+
+| Render | Shows |
+|---|---|
+| `Main.png` / `Main3.png` | A room (Living Area) on the 4-column and 3-column grid, hint bar for the focused card |
+| `Home.png` | Home overview: weather 1×2, media 1×2, vacuum, pinned devices, a row of room cards |
+| `CardSizes.png` | Allowed card sizes on the grid |
+| `Interactions.png` | Tap vs hold, media keys, every card at a glance |
+| `LightOnOffStates.png` | On/off light — states (no panel) |
+| `LightStates.png` | Dimmable light — states + panel |
+| `LightColorStates.png` | Colour light — states + panel |
+| `LedStates.png` | LED strip / TV lights — states, colour panel, white panel, All effects |
+| `ClimateStates.png` | Air conditioner — states + panel |
+| `HeatingStates.png` | Heating — radiator valve and wall thermostat: states + both panels |
+| `CurtainStates.png` | Curtain — states + panel |
+| `SwitchStates.png` | Switch / plug / lock — states (no panel) |
+| `WeatherStates.png` | Weather 1×1 and 1×2 + panel |
+| `MediaStates.png` | Media 1×1 / 1×2, seeking and volume states + panel |
+| `MediaSpec.png` | Media interaction spec (same content as ux-mechanics §B9) |
+| `RoomStates.png` | Room card (Home overview) — photo, dark, warning, no-photo states + quick panel |
+| `VacuumStates.png` | Vacuum — docked, dock needs attention, cleaning + panel |
+
+Sample names, rooms and track titles in the renders are placeholders; real data comes from Home Assistant.
+
+## Implementation notes for this repo
+
+- The native app is Rust/SDL2/OpenGL ES (see `agents.md`); there is no browser UI. Treat CSS-like
+ values in `tokens.json` (gradients, shadows) as descriptions to reproduce in the renderer.
+- Keep the logical 1920 × 1080 layout; `textScale` (1.0–1.6) multiplies type sizes only.
+- Tap vs hold must be timed from SDL key-down to key-up, acting on release for taps
+ (`ux-mechanics.md` §A1). The existing `hold` action in `src/state.rs` is the place to wire it.
+- Verify behaviour on the TV; desktop smoke tests don't prove remote timing.
+- Where the current code differs from this design (e.g. the media card's transport buttons
+ or icon set), the design wins unless a change is agreed.
+- Icons require the visible credit "Uicons by Flaticon" (Settings → About).
diff --git a/design-system/assets/roborock-s7.png b/design-system/assets/roborock-s7.png
new file mode 100644
index 0000000..d369660
Binary files /dev/null and b/design-system/assets/roborock-s7.png differ
diff --git a/design-system/components.md b/design-system/components.md
new file mode 100644
index 0000000..b01a45e
--- /dev/null
+++ b/design-system/components.md
@@ -0,0 +1,101 @@
+# Luma Home — components
+
+Layout and look of every building block. All sizes are logical px on the 1920 × 1080 canvas;
+named values are in `tokens.json`. Behaviour lives in `ux-mechanics.md` — this file only says
+what things look like. Each section names the screen render to compare against.
+
+## Screen shell
+Render: `screens/Main.png`, `screens/Home.png`
+
+| Part | Spec |
+|---|---|
+| Background | Wallpaper photo + scrim `color.background.scrim` (8,11,18 @ 80%). |
+| Clock | x 84, centred in the 72 px top bar at y 36; 20 px / 500. |
+| Top nav | Centred pill, fill `nav.pill`, 6 px padding, items 10 × 28 px padding, 20 px. Active item: fill `nav.activeTab`, ink `text.tabActiveInk`, 600. |
+| Settings button | 56 px circle, right edge at x 1836. |
+| Room tabs | Row at y 124, height 60, gap 8. Tab padding 10 × 22, 17 px. Active: fill `nav.activeRoom`, 600. Inactive ink `nav.inactive`. |
+| Grid | x 84, y 206, 1752 × 756; 3 rows of 240, gap 18; 4 columns of 424 or 3 of 572 (`grid` tokens). |
+| Hint bar | x 84, 30 px from the bottom; items 40 px apart. Key cap: 2 px border white 60%, radius 6, 11 px / 700, padding 2 × 8. Label 14 px / 500, white 80%. |
+
+## DeviceCard (1×1)
+Render: `screens/Main.png`, `screens/*States.png`
+
+- Box: fills the cell; radius 22; padding 18; border 2 px; fill/border/shadow from `color.card` (focus variants when focused).
+- Glow (on): radial gradient at (42, 42) of the accent at 38% → transparent at 72%.
+- **Top row** (gap 14): 48 px icon circle (24 px icon) · name 19 px / 600, single line, ellipsis · value 20 px / 700, right-aligned.
+- **Middle band** (flex, margin 12 top / 10 bottom), one of:
+ - `spark` — 64 px tall polyline, 3 px stroke in the accent (grey `lineOff` when off), labels "24 h" / "now" 12 px below.
+ - `bar` — 12 px track `barTrack`, radius 6, fill = accent at the level %.
+ - `swatch` — 34 px strip, radius 10: solid colour or the effect gradient.
+ - `curtain` — 5 px rail, three window panes (2 px outline white 22%), two fabric panels each `(100 − position) / 2` % wide with a 4 px edge in the accent.
+ - `image` — appliance photo on the **right** (max 120 px, square, contain), facts on the left: label 12 px white 60%, value 14 px / 700 (highlight = accent, `warn` = `accent.warning`).
+ - none — empty band (on/off lights, switches).
+- **Warning subtitle** (`subWarn`): amber `#F2B84B`, 600 — battery low, window open, dock needs attention.
+- **Offline**: whole card at 50 % opacity, value "Offline".
+- **Subtitle**: 14 px / 500, white 72%, single line, at the bottom.
+- 3-column grid: same layout in the 572 px cell.
+
+## WeatherCard
+Render: `screens/WeatherStates.png`
+
+- 1×1: weather icon, current temperature (large), condition, humidity, wind, high/low.
+- 1×2 (498 tall): the 1×1 content on top, the **5-day forecast** below (day, icon, low–high).
+
+## MediaCard
+Render: `screens/MediaStates.png`, `screens/MediaSpec.png`
+
+- Background: card fill + purple radial glow (128, 96, 192 @ 42%) from the top-left.
+- **1×1**: 84 px artwork (radius 10) · status "Playing · Spotify" 13 px / 600 `accent.mediaLabel` with the volume readout right-aligned on the same line (15 px volume icon + "24%" 14 px / 700) · title 19 px / 700 · artist 15 px.
+- **1×2**: 220 px artwork centred (radius 14) · status + volume line (20 px tall) · title 24 px · artist 17 px · elapsed / remaining 13 px under the progress bar.
+- Progress: 4 px track white 22%, fill white.
+- Buttons (gap 26, centred): Previous 52 px · Play/Pause 62 px filled `accent.media` with dark icon · Next 52 px; idle buttons white 10%.
+- Focus: ring on the focused button (4 px, offset 4, dark gap), never on the card.
+- Seeking: the held button turns white with dark icon; 16 px playhead dot with a 4 px purple halo; white bubble 14 px / 700 above it.
+- Volume change: readout becomes a white pill (dark ink, padding 0 8, radius 9–10).
+
+## VacuumCard
+Render: `screens/VacuumStates.png`
+
+DeviceCard with the `image` band: photo `assets/roborock-s7.png` (transparent cut-out, in this folder).
+Cleaning facts: Mode (highlighted), Now in. Docked facts: Battery, Water · clean / dirty.
+
+## RoomCard
+Render: `screens/RoomStates.png`, `screens/Home.png`
+
+- Box: as DeviceCard. Background: the Home Assistant area photo (fetched from HA, never bundled — `ux-mechanics.md` §B11), cover-cropped, under a gradient
+ `rgba(8,11,18,.78) 0% → 0 at 32% → 0 at 40% → .84 at 66% → .94 at 100%`. Lights off: photo `brightness(0.45) saturate(0.2)`.
+ No photo: glass fill + gold glow while any light is on.
+- Top row as DeviceCard; text gets a soft shadow `0 1px 6px rgba(0,0,0,.75)` over a photo.
+- Bottom line (8 px above the subtitle): three stats, gap 22, each a 20 px icon + value 18 px / 700. Colours: lights `accent.light`, curtains `accent.curtain` (white 75 % when closed), air `aqGood` / `aqFair` / `aqPoor`.
+- Subtitle 14 px, white 78 %; warnings `warning` 600.
+
+## Panel (shared)
+Renders: every `*States.png` (right side)
+
+- 620 × 1080 at the right edge, radius 28, fill `panel.fill`, shadow `shadow.panel`, padding 56 / 56 / 48.
+- Header: eyebrow (room) 16 px white 72% · 56 px icon circle (accent fill, 30 px icon) · title 34 px / 700 · state line 18 px / 600 in the accent.
+- Sections: label 19 px / 600, 44 px below the header, 40 px between sections, control 14 px below its label.
+- Slider: 72 px tall, radius 18, track white 12%, fill accent, value 28 px / 700 inside on the left. Help line 15 px white 74% below.
+- Buttons / presets: 64 px tall, radius 16, gap 12, 20 px / 600, fill white 10%. Selected: accent at 26% fill + 3 px inset accent edge (or solid accent with dark ink for the primary choice).
+- Focus inside panels: 4 px white ring, offset 5, with a 5 px dark gap.
+- Hint bar pinned to the bottom of the panel (same style as the screen hint bar).
+
+### Panel contents
+| Panel | Sections (top → bottom) | Render |
+|---|---|---|
+| Dimmable light | Brightness · Presets (Off, 25, 50, 100 %) | `LightStates.png` |
+| Colour light | Brightness · Light type (White / Colour) · White temperature or Hue · Swatches | `LightColorStates.png` |
+| LED / TV lights | Brightness · Light · Temperature or Hue · White or Colour presets · Effects (6 pinned + All effects) | `LedStates.png` (colour, white, all effects) |
+| All effects | Pinned group (numbered, "Not on panel" tag after 6) · All effects A–Z; gold star = pinned | `LedStates.png` (right) |
+| AC | Target · Mode (5 icons) · Fan speed · auto (Auto, Quiet, Turbo) · Fan speed · manual (5 steps) · Up–down airflow · Left–right airflow (Swing + 5, icon-only) | `ClimateStates.png` |
+| Heating | Target dial (with room temp) · Mode (Off / Heat) · Preset · Temperature sensor (thermostat) · Settings toggles · battery / info line | `HeatingStates.png` |
+| Curtain | Window drawing + position · Open / Stop / Close · Presets (Open, 60, 45, 30, Closed) | `CurtainStates.png` |
+| Weather | Now · Humidity · Wind · Next hours · Next days | `WeatherStates.png` |
+| Media | Now playing · Play on · Up next · Search | `MediaStates.png` |
+| Room quick panel | Header (temperature · humidity · air) · Lights with All off + one switch row each · Curtains rows with Open / Stop / Close · Climate rows with switches | `RoomStates.png` |
+| Vacuum | Rooms (3-column grid, check + green edge when picked) · Mode · Routines + Dock · primary "Clean N rooms" (solid green, 64 px) | `VacuumStates.png` |
+
+## Icons
+`icons/` — 58 single-colour SVGs (Uicons Regular Rounded + two custom), tinted at render time:
+dark ink `#15181E` on an accent circle when on, white 80% on a grey circle when off.
+See `icons/README.md` for the name map and the required attribution.
diff --git a/design-system/icons/README.md b/design-system/icons/README.md
new file mode 100644
index 0000000..ef84d3c
--- /dev/null
+++ b/design-system/icons/README.md
@@ -0,0 +1,67 @@
+Uicons by Flaticon, Regular Rounded (https://www.flaticon.com/uicons), renamed to Luma's icon names. Drawn in white (#ffffff) as single-ink SVGs on a 300-unit square; tint them at render time (accent, accent-off, weather-icon, info-blue).
+
+Free use requires the credit "Uicons by Flaticon" with a link to flaticon.com/uicons, visible in the app (Settings → About).
+
+| Luma name | Uicons |
+|---|---|
+| `ac` | `fi-rr-air-conditioner` |
+| `apps` | `fi-rr-apps` |
+| `battery` | `fi-rr-battery-half` |
+| `clean` | `fi-rr-broom` |
+| `climate` | `fi-rr-thermometer-half` |
+| `close` | `fi-rr-cross` |
+| `cover` | `fi-rr-blinds` |
+| `cover-open` | `fi-rr-blinds-open` |
+| `direction` | `fi-rr-window-frame` |
+| `effect` | `fi-rr-sparkles` |
+| `home` | `fi-rr-home` |
+| `humidity` | `fi-rr-raindrops` |
+| `input` | `fi-rr-plug` |
+| `led-strip` | `fi-rr-rainbow` |
+| `light` | `fi-rr-bulb` |
+| `light-ceiling` | `fi-rr-light-ceiling` |
+| `light-on` | `fi-rr-lightbulb-on` |
+| `location` | `fi-rr-marker` |
+| `lock` | `fi-rr-lock` |
+| `minus` | `fi-rr-minus` |
+| `mode-auto` | `fi-rr-refresh` |
+| `mode-cool` | `fi-rr-snowflake` |
+| `mode-dry` | `fi-rr-sun` |
+| `mode-fan` | `fi-rr-fan` |
+| `mode-heat` | `fi-rr-temperature-high` |
+| `more` | `fi-rr-menu-dots` |
+| `pause` | `fi-rr-pause` |
+| `play` | `fi-rr-play` |
+| `plus` | `fi-rr-plus` |
+| `power` | `fi-rr-power` |
+| `queue` | `fi-rr-menu-burger` |
+| `scene` | `fi-rr-sparkles` |
+| `search` | `fi-rr-search` |
+| `seek-back` | `fi-rr-rewind` |
+| `seek-forward` | `fi-rr-forward` |
+| `settings` | `fi-rr-settings-sliders` |
+| `skip-back` | `fi-rr-step-backward` |
+| `skip-forward` | `fi-rr-step-forward` |
+| `switch-off` | `fi-rr-toggle-off` |
+| `switch-on` | `fi-rr-toggle-on` |
+| `tv` | `fi-rr-tv-retro` |
+| `unlock` | `fi-rr-unlock` |
+| `vacuum` | `fi-rr-vacuum-robot` |
+| `volume` | `fi-rr-volume` |
+| `volume-mute` | `fi-rr-volume-mute` |
+| `weather-cloud` | `fi-rr-cloud` |
+| `weather-drizzle` | `fi-rr-cloud-drizzle` |
+| `weather-fog` | `fi-rr-fog` |
+| `weather-moon` | `fi-rr-moon` |
+| `weather-partly-cloudy` | `fi-rr-cloud-sun` |
+| `weather-rain` | `fi-rr-cloud-rain` |
+| `weather-snow` | `fi-rr-cloud-snow` |
+| `weather-storm` | `fi-rr-thunderstorm` |
+| `weather-sun` | `fi-rr-sun` |
+| `weather-wind` | `fi-rr-wind` |
+| `quiet` | custom (fan with low waves) — AC "Quiet" preset |
+| `window` | `fi-rr-window-frame` (same as `direction`) — open-window detection |
+| `turbo` | custom (fan with speed lines) — AC "Turbo" preset |
+
+All SVGs use a 300 × 300 viewBox (except `mode-heat`, which is cropped to `25 0 300 300`).
+In the native app compile them to `.lvec` like the other vector assets (`tools/vector-assets/`).
diff --git a/design-system/icons/ac.svg b/design-system/icons/ac.svg
new file mode 100644
index 0000000..97793a2
--- /dev/null
+++ b/design-system/icons/ac.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/apps.svg b/design-system/icons/apps.svg
new file mode 100644
index 0000000..aaccbcf
--- /dev/null
+++ b/design-system/icons/apps.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/battery.svg b/design-system/icons/battery.svg
new file mode 100644
index 0000000..7225255
--- /dev/null
+++ b/design-system/icons/battery.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/clean.svg b/design-system/icons/clean.svg
new file mode 100644
index 0000000..8a25103
--- /dev/null
+++ b/design-system/icons/clean.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/climate.svg b/design-system/icons/climate.svg
new file mode 100644
index 0000000..900a592
--- /dev/null
+++ b/design-system/icons/climate.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/close.svg b/design-system/icons/close.svg
new file mode 100644
index 0000000..d6ff65e
--- /dev/null
+++ b/design-system/icons/close.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/cover-open.svg b/design-system/icons/cover-open.svg
new file mode 100644
index 0000000..1d08384
--- /dev/null
+++ b/design-system/icons/cover-open.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/cover.svg b/design-system/icons/cover.svg
new file mode 100644
index 0000000..0887b69
--- /dev/null
+++ b/design-system/icons/cover.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/direction.svg b/design-system/icons/direction.svg
new file mode 100644
index 0000000..2d41a6b
--- /dev/null
+++ b/design-system/icons/direction.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/effect.svg b/design-system/icons/effect.svg
new file mode 100644
index 0000000..3c0860e
--- /dev/null
+++ b/design-system/icons/effect.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/home.svg b/design-system/icons/home.svg
new file mode 100644
index 0000000..36e8143
--- /dev/null
+++ b/design-system/icons/home.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/humidity.svg b/design-system/icons/humidity.svg
new file mode 100644
index 0000000..d1f84a7
--- /dev/null
+++ b/design-system/icons/humidity.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/input.svg b/design-system/icons/input.svg
new file mode 100644
index 0000000..c10fa22
--- /dev/null
+++ b/design-system/icons/input.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/led-strip.svg b/design-system/icons/led-strip.svg
new file mode 100644
index 0000000..7e4c7da
--- /dev/null
+++ b/design-system/icons/led-strip.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/light-ceiling.svg b/design-system/icons/light-ceiling.svg
new file mode 100644
index 0000000..48ee40c
--- /dev/null
+++ b/design-system/icons/light-ceiling.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/light-on.svg b/design-system/icons/light-on.svg
new file mode 100644
index 0000000..b7c475e
--- /dev/null
+++ b/design-system/icons/light-on.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/light.svg b/design-system/icons/light.svg
new file mode 100644
index 0000000..478c6c1
--- /dev/null
+++ b/design-system/icons/light.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/location.svg b/design-system/icons/location.svg
new file mode 100644
index 0000000..d29d171
--- /dev/null
+++ b/design-system/icons/location.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/lock.svg b/design-system/icons/lock.svg
new file mode 100644
index 0000000..930280e
--- /dev/null
+++ b/design-system/icons/lock.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/minus.svg b/design-system/icons/minus.svg
new file mode 100644
index 0000000..043d625
--- /dev/null
+++ b/design-system/icons/minus.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/mode-auto.svg b/design-system/icons/mode-auto.svg
new file mode 100644
index 0000000..a65bd9d
--- /dev/null
+++ b/design-system/icons/mode-auto.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/mode-cool.svg b/design-system/icons/mode-cool.svg
new file mode 100644
index 0000000..0e3da6b
--- /dev/null
+++ b/design-system/icons/mode-cool.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/mode-dry.svg b/design-system/icons/mode-dry.svg
new file mode 100644
index 0000000..9dc42f7
--- /dev/null
+++ b/design-system/icons/mode-dry.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/mode-fan.svg b/design-system/icons/mode-fan.svg
new file mode 100644
index 0000000..fe2e9ff
--- /dev/null
+++ b/design-system/icons/mode-fan.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/mode-heat.svg b/design-system/icons/mode-heat.svg
new file mode 100644
index 0000000..02cd194
--- /dev/null
+++ b/design-system/icons/mode-heat.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/more.svg b/design-system/icons/more.svg
new file mode 100644
index 0000000..8b130a9
--- /dev/null
+++ b/design-system/icons/more.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/pause.svg b/design-system/icons/pause.svg
new file mode 100644
index 0000000..e10cdd2
--- /dev/null
+++ b/design-system/icons/pause.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/play.svg b/design-system/icons/play.svg
new file mode 100644
index 0000000..89b869e
--- /dev/null
+++ b/design-system/icons/play.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/plus.svg b/design-system/icons/plus.svg
new file mode 100644
index 0000000..4f248a2
--- /dev/null
+++ b/design-system/icons/plus.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/power.svg b/design-system/icons/power.svg
new file mode 100644
index 0000000..e685ecc
--- /dev/null
+++ b/design-system/icons/power.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/queue.svg b/design-system/icons/queue.svg
new file mode 100644
index 0000000..1b4e6e9
--- /dev/null
+++ b/design-system/icons/queue.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/quiet.svg b/design-system/icons/quiet.svg
new file mode 100644
index 0000000..a5fbdbd
--- /dev/null
+++ b/design-system/icons/quiet.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/scene.svg b/design-system/icons/scene.svg
new file mode 100644
index 0000000..527fbe3
--- /dev/null
+++ b/design-system/icons/scene.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/search.svg b/design-system/icons/search.svg
new file mode 100644
index 0000000..a9a337b
--- /dev/null
+++ b/design-system/icons/search.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/seek-back.svg b/design-system/icons/seek-back.svg
new file mode 100644
index 0000000..8dac578
--- /dev/null
+++ b/design-system/icons/seek-back.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/seek-forward.svg b/design-system/icons/seek-forward.svg
new file mode 100644
index 0000000..025b35d
--- /dev/null
+++ b/design-system/icons/seek-forward.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/settings.svg b/design-system/icons/settings.svg
new file mode 100644
index 0000000..d91acd5
--- /dev/null
+++ b/design-system/icons/settings.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/skip-back.svg b/design-system/icons/skip-back.svg
new file mode 100644
index 0000000..11f36e2
--- /dev/null
+++ b/design-system/icons/skip-back.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/skip-forward.svg b/design-system/icons/skip-forward.svg
new file mode 100644
index 0000000..8e4dd56
--- /dev/null
+++ b/design-system/icons/skip-forward.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/switch-off.svg b/design-system/icons/switch-off.svg
new file mode 100644
index 0000000..027b288
--- /dev/null
+++ b/design-system/icons/switch-off.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/switch-on.svg b/design-system/icons/switch-on.svg
new file mode 100644
index 0000000..e2b2f03
--- /dev/null
+++ b/design-system/icons/switch-on.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/turbo.svg b/design-system/icons/turbo.svg
new file mode 100644
index 0000000..27739cb
--- /dev/null
+++ b/design-system/icons/turbo.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/tv.svg b/design-system/icons/tv.svg
new file mode 100644
index 0000000..d2bcd89
--- /dev/null
+++ b/design-system/icons/tv.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/unlock.svg b/design-system/icons/unlock.svg
new file mode 100644
index 0000000..fc2e96d
--- /dev/null
+++ b/design-system/icons/unlock.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/vacuum.svg b/design-system/icons/vacuum.svg
new file mode 100644
index 0000000..192dbb9
--- /dev/null
+++ b/design-system/icons/vacuum.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/volume-mute.svg b/design-system/icons/volume-mute.svg
new file mode 100644
index 0000000..4a04a02
--- /dev/null
+++ b/design-system/icons/volume-mute.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/volume.svg b/design-system/icons/volume.svg
new file mode 100644
index 0000000..f2058b0
--- /dev/null
+++ b/design-system/icons/volume.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-cloud.svg b/design-system/icons/weather-cloud.svg
new file mode 100644
index 0000000..48ad161
--- /dev/null
+++ b/design-system/icons/weather-cloud.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-drizzle.svg b/design-system/icons/weather-drizzle.svg
new file mode 100644
index 0000000..0bbb244
--- /dev/null
+++ b/design-system/icons/weather-drizzle.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-fog.svg b/design-system/icons/weather-fog.svg
new file mode 100644
index 0000000..e1f6069
--- /dev/null
+++ b/design-system/icons/weather-fog.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-moon.svg b/design-system/icons/weather-moon.svg
new file mode 100644
index 0000000..423768d
--- /dev/null
+++ b/design-system/icons/weather-moon.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-partly-cloudy.svg b/design-system/icons/weather-partly-cloudy.svg
new file mode 100644
index 0000000..3ebf285
--- /dev/null
+++ b/design-system/icons/weather-partly-cloudy.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-rain.svg b/design-system/icons/weather-rain.svg
new file mode 100644
index 0000000..5ed079e
--- /dev/null
+++ b/design-system/icons/weather-rain.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-snow.svg b/design-system/icons/weather-snow.svg
new file mode 100644
index 0000000..d44714c
--- /dev/null
+++ b/design-system/icons/weather-snow.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-storm.svg b/design-system/icons/weather-storm.svg
new file mode 100644
index 0000000..1671072
--- /dev/null
+++ b/design-system/icons/weather-storm.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-sun.svg b/design-system/icons/weather-sun.svg
new file mode 100644
index 0000000..4bb37fa
--- /dev/null
+++ b/design-system/icons/weather-sun.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/weather-wind.svg b/design-system/icons/weather-wind.svg
new file mode 100644
index 0000000..35a12be
--- /dev/null
+++ b/design-system/icons/weather-wind.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/icons/window.svg b/design-system/icons/window.svg
new file mode 100644
index 0000000..a5359e4
--- /dev/null
+++ b/design-system/icons/window.svg
@@ -0,0 +1 @@
+
diff --git a/design-system/screens/CardSizes.png b/design-system/screens/CardSizes.png
new file mode 100644
index 0000000..43ac98d
Binary files /dev/null and b/design-system/screens/CardSizes.png differ
diff --git a/design-system/screens/ClimateStates.png b/design-system/screens/ClimateStates.png
new file mode 100644
index 0000000..f8f9d14
Binary files /dev/null and b/design-system/screens/ClimateStates.png differ
diff --git a/design-system/screens/CurtainStates.png b/design-system/screens/CurtainStates.png
new file mode 100644
index 0000000..e2566bb
Binary files /dev/null and b/design-system/screens/CurtainStates.png differ
diff --git a/design-system/screens/HeatingStates.png b/design-system/screens/HeatingStates.png
new file mode 100644
index 0000000..af13d66
Binary files /dev/null and b/design-system/screens/HeatingStates.png differ
diff --git a/design-system/screens/Home.png b/design-system/screens/Home.png
new file mode 100644
index 0000000..7aa4d97
Binary files /dev/null and b/design-system/screens/Home.png differ
diff --git a/design-system/screens/Interactions.png b/design-system/screens/Interactions.png
new file mode 100644
index 0000000..ab485e0
Binary files /dev/null and b/design-system/screens/Interactions.png differ
diff --git a/design-system/screens/LedStates.png b/design-system/screens/LedStates.png
new file mode 100644
index 0000000..7dcf16f
Binary files /dev/null and b/design-system/screens/LedStates.png differ
diff --git a/design-system/screens/LightColorStates.png b/design-system/screens/LightColorStates.png
new file mode 100644
index 0000000..61592a4
Binary files /dev/null and b/design-system/screens/LightColorStates.png differ
diff --git a/design-system/screens/LightOnOffStates.png b/design-system/screens/LightOnOffStates.png
new file mode 100644
index 0000000..2315f26
Binary files /dev/null and b/design-system/screens/LightOnOffStates.png differ
diff --git a/design-system/screens/LightStates.png b/design-system/screens/LightStates.png
new file mode 100644
index 0000000..63272b8
Binary files /dev/null and b/design-system/screens/LightStates.png differ
diff --git a/design-system/screens/Main.png b/design-system/screens/Main.png
new file mode 100644
index 0000000..a767aed
Binary files /dev/null and b/design-system/screens/Main.png differ
diff --git a/design-system/screens/Main3.png b/design-system/screens/Main3.png
new file mode 100644
index 0000000..a9aa56e
Binary files /dev/null and b/design-system/screens/Main3.png differ
diff --git a/design-system/screens/MediaSpec.png b/design-system/screens/MediaSpec.png
new file mode 100644
index 0000000..87e930c
Binary files /dev/null and b/design-system/screens/MediaSpec.png differ
diff --git a/design-system/screens/MediaStates.png b/design-system/screens/MediaStates.png
new file mode 100644
index 0000000..a8ee477
Binary files /dev/null and b/design-system/screens/MediaStates.png differ
diff --git a/design-system/screens/RoomStates.png b/design-system/screens/RoomStates.png
new file mode 100644
index 0000000..6ec3605
Binary files /dev/null and b/design-system/screens/RoomStates.png differ
diff --git a/design-system/screens/SwitchStates.png b/design-system/screens/SwitchStates.png
new file mode 100644
index 0000000..3bc00a7
Binary files /dev/null and b/design-system/screens/SwitchStates.png differ
diff --git a/design-system/screens/VacuumStates.png b/design-system/screens/VacuumStates.png
new file mode 100644
index 0000000..02fcac2
Binary files /dev/null and b/design-system/screens/VacuumStates.png differ
diff --git a/design-system/screens/WeatherStates.png b/design-system/screens/WeatherStates.png
new file mode 100644
index 0000000..78d633f
Binary files /dev/null and b/design-system/screens/WeatherStates.png differ
diff --git a/design-system/tokens.css b/design-system/tokens.css
new file mode 100644
index 0000000..4e9b39f
--- /dev/null
+++ b/design-system/tokens.css
@@ -0,0 +1,176 @@
+/* Generated from tokens.json. For the Lovelace cards (cards/*.js) and web previews; the native renderer reads tokens.json. */
+:root {
+ --luma-screen-width: 1920px;
+ --luma-screen-height: 1080px;
+ --luma-screen-margin-x: 84px;
+ --luma-screen-top-nav-y: 36px;
+ --luma-screen-top-nav-height: 72px;
+ --luma-screen-room-tabs-y: 124px;
+ --luma-screen-room-tabs-height: 60px;
+ --luma-screen-hint-bar-x: 84px;
+ --luma-screen-hint-bar-bottom: 30px;
+ --luma-grid-x: 84px;
+ --luma-grid-y: 206px;
+ --luma-grid-width: 1752px;
+ --luma-grid-height: 756px;
+ --luma-grid-rows: 3;
+ --luma-grid-gap: 18px;
+ --luma-grid-four-columns-columns: 4;
+ --luma-grid-four-columns-cell-w: 424px;
+ --luma-grid-four-columns-cell-h: 240px;
+ --luma-grid-three-columns-columns: 3;
+ --luma-grid-three-columns-cell-w: 572px;
+ --luma-grid-three-columns-cell-h: 240px;
+ --luma-grid-tall-card-h: 498px;
+ --luma-color-background-wallpaper-fallback: linear-gradient(160deg, #6D7F99 0%, #45526A 42%, #2B313C 100%);
+ --luma-color-background-scrim: rgba(8, 11, 18, 0.80);
+ --luma-color-background-spec-board: #161A21;
+ --luma-color-card-fill: rgba(34, 38, 46, 0.88);
+ --luma-color-card-fill-focus: rgba(54, 60, 72, 0.96);
+ --luma-color-card-border: rgba(255, 255, 255, 0.10);
+ --luma-color-card-border-focus: rgba(255, 255, 255, 0.24);
+ --luma-color-card-icon-circle-off: rgba(255, 255, 255, 0.10);
+ --luma-color-card-icon-off: rgba(255, 255, 255, 0.80);
+ --luma-color-card-icon-on: #15181E;
+ --luma-color-card-value-off: rgba(255, 255, 255, 0.60);
+ --luma-color-card-glow-on: radial-gradient(circle at 42px 42px,
, transparent 72%);
+ --luma-color-card-bar-track: rgba(255, 255, 255, 0.14);
+ --luma-color-card-line-off: rgba(255, 255, 255, 0.45);
+ --luma-color-panel-fill: rgba(30, 34, 42, 0.98);
+ --luma-color-panel-control: rgba(255, 255, 255, 0.10);
+ --luma-color-panel-control-selected-fill: ;
+ --luma-color-panel-control-selected-edge: inset 3px ;
+ --luma-color-panel-section-label: rgba(255, 255, 255, 0.72);
+ --luma-color-panel-rule: rgba(255, 255, 255, 0.08);
+ --luma-color-text-primary: #FFFFFF;
+ --luma-color-text-secondary: rgba(255, 255, 255, 0.78);
+ --luma-color-text-subtitle: rgba(255, 255, 255, 0.72);
+ --luma-color-text-tertiary: rgba(255, 255, 255, 0.60);
+ --luma-color-text-on-light: #15181E;
+ --luma-color-text-tab-active-ink: #1F2630;
+ --luma-color-focus-ring: rgba(255, 255, 255, 0.96);
+ --luma-color-focus-gap: rgba(8, 11, 18, 0.85);
+ --luma-color-focus-gap-in-panel: #1E222A;
+ --luma-color-nav-pill: rgba(36, 40, 48, 0.90);
+ --luma-color-nav-active-tab: rgba(240, 244, 247, 0.95);
+ --luma-color-nav-active-room: rgba(255, 255, 255, 0.20);
+ --luma-color-nav-inactive: rgba(255, 255, 255, 0.72);
+ --luma-color-accent-light: #F5B85A;
+ --luma-color-accent-light-warm-white: #F7C27A;
+ --luma-color-accent-heat: #F06A4A;
+ --luma-color-accent-cool: #4C9BF0;
+ --luma-color-accent-switch: #7EB8DA;
+ --luma-color-accent-lock: #60C090;
+ --luma-color-accent-curtain: #DCD2B4;
+ --luma-color-accent-led: #E07A5F;
+ --luma-color-accent-media: #9B7DDB;
+ --luma-color-accent-media-label: #B9A2EC;
+ --luma-color-accent-vacuum: #60C090;
+ --luma-color-accent-warning: #F2B84B;
+ --luma-color-accent-effect-playing: #FF8A75;
+ --luma-color-accent-aq-good: #3DDC97;
+ --luma-color-accent-aq-fair: #F2B84B;
+ --luma-color-accent-aq-poor: #FF6B5A;
+ --luma-color-swatches-colour-1: #FF5A40;
+ --luma-color-swatches-colour-2: #F0A050;
+ --luma-color-swatches-colour-3: #F2D04B;
+ --luma-color-swatches-colour-4: #3DDC97;
+ --luma-color-swatches-colour-5: #4C9BF0;
+ --luma-color-swatches-colour-6: #7B5BE0;
+ --luma-color-swatches-colour-7: #E45AD8;
+ --luma-color-swatches-colour-8: #FFFFFF;
+ --luma-color-swatches-white-2700k: #FFB46B;
+ --luma-color-swatches-white-3000k: #FFC98F;
+ --luma-color-swatches-white-4000k: #FFE2C0;
+ --luma-color-swatches-white-5000k: #FFF4E6;
+ --luma-color-swatches-white-6500k: #E3EEFF;
+ --luma-color-room-photo-gradient: linear-gradient(180deg, rgba(8,11,18,0.78) 0%, rgba(8,11,18,0) 32%, rgba(8,11,18,0) 40%, rgba(8,11,18,0.84) 66%, rgba(8,11,18,0.94) 100%);
+ --luma-color-room-photo-dark-filter: brightness(0.45) saturate(0.2);
+ --luma-color-room-photo-text-shadow: 0 1px 6px rgba(0,0,0,0.75);
+ --luma-type-family: Inter (fallback: system sans);
+ --luma-type-weights-regular: 400;
+ --luma-type-weights-medium: 500;
+ --luma-type-weights-semibold: 600;
+ --luma-type-weights-bold: 700;
+ --luma-type-size-clock: 20px;
+ --luma-type-size-top-nav: 20px;
+ --luma-type-size-room-tab: 17px;
+ --luma-type-size-card-name: 19px;
+ --luma-type-size-card-value: 20px;
+ --luma-type-size-card-subtitle: 14px;
+ --luma-type-size-card-fact: 14px;
+ --luma-type-size-card-fact-label: 12px;
+ --luma-type-size-panel-eyebrow: 16px;
+ --luma-type-size-panel-title: 34px;
+ --luma-type-size-panel-section: 19px;
+ --luma-type-size-panel-value: 19px;
+ --luma-type-size-hint: 14px;
+ --luma-type-size-hint-key: 11px;
+ --luma-type-size-media-title-small: 19px;
+ --luma-type-size-media-title-large: 24px;
+ --luma-type-size-media-artist-small: 15px;
+ --luma-type-size-media-artist-large: 17px;
+ --luma-type-size-media-status: 13px;
+ --luma-type-size-panel-state: 18px;
+ --luma-type-size-panel-help: 15px;
+ --luma-type-size-panel-slider-value: 28px;
+ --luma-type-size-panel-button: 20px;
+ --luma-type-text-scale-default: 1;
+ --luma-type-text-scale-min: 1;
+ --luma-type-text-scale-max: 1.6;
+ --luma-size-card-padding: 18px;
+ --luma-size-card-radius: 22px;
+ --luma-size-icon-circle: 48px;
+ --luma-size-icon: 24px;
+ --luma-size-card-inner-gap: 14px;
+ --luma-size-middle-band-margin: 12 0 10;
+ --luma-size-level-bar: 12px;
+ --luma-size-colour-strip: 34px;
+ --luma-size-spark-height: 64px;
+ --luma-size-panel-width: 620px;
+ --luma-size-panel-height: 1080px;
+ --luma-size-panel-radius: 28px;
+ --luma-size-panel-padding: 56 56 48;
+ --luma-size-panel-header-icon: 56px;
+ --luma-size-panel-header-icon-glyph: 30px;
+ --luma-size-panel-section-gap: 40px;
+ --luma-size-panel-label-to-control: 14px;
+ --luma-size-panel-slider-height: 72px;
+ --luma-size-panel-slider-radius: 18px;
+ --luma-size-panel-button-height: 64px;
+ --luma-size-panel-button-radius: 16px;
+ --luma-size-panel-button-gap: 12px;
+ --luma-size-panel-list-row-height: 54px;
+ --luma-size-panel-focus-ring-offset: 5px;
+ --luma-size-media-art-small: 84px;
+ --luma-size-media-art-large: 220px;
+ --luma-size-media-button-small: 52px;
+ --luma-size-media-button-play: 62px;
+ --luma-size-media-progress: 4px;
+ --luma-size-focus-ring-width: 4px;
+ --luma-size-focus-ring-offset: 4px;
+ --luma-size-focus-ring-card-scale: 1.04px;
+ --luma-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.25);
+ --luma-shadow-card-focus: 0 16px 36px rgba(0, 0, 0, 0.50);
+ --luma-shadow-panel: 0 24px 60px rgba(0, 0, 0, 0.50);
+ --luma-motion-focus: 120ms ease-out;
+ --luma-motion-panel-open: 200ms ease-out, slide from right;
+ --luma-motion-bubble-fade: 1000ms after release;
+ --luma-timing-hold-threshold-ms: 400ms;
+ --luma-timing-arrow-repeat-delay-ms: 400ms;
+ --luma-timing-arrow-repeat-ms: 150ms;
+ --luma-timing-step-double-after-ms: 2000ms;
+ --luma-timing-send-debounce-ms: 200ms;
+ --luma-timing-confirm-timeout-ms: 5000ms;
+ --luma-timing-error-hint-ms: 4000ms;
+ --luma-timing-volume-pill-ms: 1500ms;
+ --luma-timing-seek-step-ms: 300ms;
+ --luma-timing-seek-step-s: 10;
+ --luma-timing-seek-step-fast-s: 30;
+ --luma-timing-seek-fast-after-ms: 2000ms;
+ --luma-timing-previous-restart-after-s: 3;
+ --luma-timing-volume-step-pct: 2;
+ --luma-air-quality-metric: CO2 ppm;
+ --luma-air-quality-good-below: 800;
+ --luma-air-quality-poor-above: 1200;
+}
diff --git a/design-system/tokens.json b/design-system/tokens.json
new file mode 100644
index 0000000..aefc6df
--- /dev/null
+++ b/design-system/tokens.json
@@ -0,0 +1,252 @@
+{
+ "$meta": {
+ "name": "Luma Home",
+ "version": 2,
+ "units": "px at a logical 1920×1080 canvas",
+ "source": "Luma — 10-foot redesign (Design canvas), 2026-10-27",
+ "note": "Values are renderer-agnostic. Colours are sRGB; rgba alpha 0–1."
+ },
+ "screen": {
+ "width": 1920,
+ "height": 1080,
+ "marginX": 84,
+ "topNav": {
+ "y": 36,
+ "height": 72
+ },
+ "roomTabs": {
+ "y": 124,
+ "height": 60
+ },
+ "hintBar": {
+ "x": 84,
+ "bottom": 30
+ }
+ },
+ "grid": {
+ "x": 84,
+ "y": 206,
+ "width": 1752,
+ "height": 756,
+ "rows": 3,
+ "gap": 18,
+ "fourColumns": {
+ "columns": 4,
+ "cell": {
+ "w": 424,
+ "h": 240
+ }
+ },
+ "threeColumns": {
+ "columns": 3,
+ "cell": {
+ "w": 572,
+ "h": 240
+ }
+ },
+ "tallCard": {
+ "h": 498,
+ "note": "1×2 = two rows + one gap"
+ }
+ },
+ "color": {
+ "background": {
+ "wallpaperFallback": "linear-gradient(160deg, #6D7F99 0%, #45526A 42%, #2B313C 100%)",
+ "scrim": "rgba(8, 11, 18, 0.80)",
+ "specBoard": "#161A21"
+ },
+ "card": {
+ "fill": "rgba(34, 38, 46, 0.88)",
+ "fillFocus": "rgba(54, 60, 72, 0.96)",
+ "border": "rgba(255, 255, 255, 0.10)",
+ "borderFocus": "rgba(255, 255, 255, 0.24)",
+ "iconCircleOff": "rgba(255, 255, 255, 0.10)",
+ "iconOff": "rgba(255, 255, 255, 0.80)",
+ "iconOn": "#15181E",
+ "valueOff": "rgba(255, 255, 255, 0.60)",
+ "glowOn": "radial-gradient(circle at 42px 42px, , transparent 72%)",
+ "barTrack": "rgba(255, 255, 255, 0.14)",
+ "lineOff": "rgba(255, 255, 255, 0.45)"
+ },
+ "panel": {
+ "fill": "rgba(30, 34, 42, 0.98)",
+ "control": "rgba(255, 255, 255, 0.10)",
+ "controlSelectedFill": "",
+ "controlSelectedEdge": "inset 3px ",
+ "sectionLabel": "rgba(255, 255, 255, 0.72)",
+ "rule": "rgba(255, 255, 255, 0.08)"
+ },
+ "text": {
+ "primary": "#FFFFFF",
+ "secondary": "rgba(255, 255, 255, 0.78)",
+ "subtitle": "rgba(255, 255, 255, 0.72)",
+ "tertiary": "rgba(255, 255, 255, 0.60)",
+ "onLight": "#15181E",
+ "tabActiveInk": "#1F2630"
+ },
+ "focus": {
+ "ring": "rgba(255, 255, 255, 0.96)",
+ "gap": "rgba(8, 11, 18, 0.85)",
+ "gapInPanel": "#1E222A"
+ },
+ "nav": {
+ "pill": "rgba(36, 40, 48, 0.90)",
+ "activeTab": "rgba(240, 244, 247, 0.95)",
+ "activeRoom": "rgba(255, 255, 255, 0.20)",
+ "inactive": "rgba(255, 255, 255, 0.72)"
+ },
+ "accent": {
+ "light": "#F5B85A",
+ "lightWarmWhite": "#F7C27A",
+ "heat": "#F06A4A",
+ "cool": "#4C9BF0",
+ "switch": "#7EB8DA",
+ "lock": "#60C090",
+ "curtain": "#DCD2B4",
+ "led": "#E07A5F",
+ "media": "#9B7DDB",
+ "mediaLabel": "#B9A2EC",
+ "vacuum": "#60C090",
+ "warning": "#F2B84B",
+ "effectPlaying": "#FF8A75",
+ "aqGood": "#3DDC97",
+ "aqFair": "#F2B84B",
+ "aqPoor": "#FF6B5A"
+ },
+ "swatches": {
+ "colour": [
+ "#FF5A40",
+ "#F0A050",
+ "#F2D04B",
+ "#3DDC97",
+ "#4C9BF0",
+ "#7B5BE0",
+ "#E45AD8",
+ "#FFFFFF"
+ ],
+ "white": {
+ "2700 K": "#FFB46B",
+ "3000 K": "#FFC98F",
+ "4000 K": "#FFE2C0",
+ "5000 K": "#FFF4E6",
+ "6500 K": "#E3EEFF"
+ }
+ },
+ "roomPhoto": {
+ "gradient": "linear-gradient(180deg, rgba(8,11,18,0.78) 0%, rgba(8,11,18,0) 32%, rgba(8,11,18,0) 40%, rgba(8,11,18,0.84) 66%, rgba(8,11,18,0.94) 100%)",
+ "darkFilter": "brightness(0.45) saturate(0.2)",
+ "textShadow": "0 1px 6px rgba(0,0,0,0.75)"
+ }
+ },
+ "type": {
+ "family": "Inter (fallback: system sans)",
+ "weights": {
+ "regular": 400,
+ "medium": 500,
+ "semibold": 600,
+ "bold": 700
+ },
+ "size": {
+ "clock": 20,
+ "topNav": 20,
+ "roomTab": 17,
+ "cardName": 19,
+ "cardValue": 20,
+ "cardSubtitle": 14,
+ "cardFact": 14,
+ "cardFactLabel": 12,
+ "panelEyebrow": 16,
+ "panelTitle": 34,
+ "panelSection": 19,
+ "panelValue": 19,
+ "hint": 14,
+ "hintKey": 11,
+ "mediaTitleSmall": 19,
+ "mediaTitleLarge": 24,
+ "mediaArtistSmall": 15,
+ "mediaArtistLarge": 17,
+ "mediaStatus": 13,
+ "panelState": 18,
+ "panelHelp": 15,
+ "panelSliderValue": 28,
+ "panelButton": 20
+ },
+ "textScale": {
+ "default": 1,
+ "min": 1,
+ "max": 1.6,
+ "note": "User setting; multiply all type sizes."
+ }
+ },
+ "size": {
+ "cardPadding": 18,
+ "cardRadius": 22,
+ "iconCircle": 48,
+ "icon": 24,
+ "cardInnerGap": 14,
+ "middleBandMargin": "12 0 10",
+ "levelBar": 12,
+ "colourStrip": 34,
+ "sparkHeight": 64,
+ "panel": {
+ "width": 620,
+ "height": 1080,
+ "radius": 28,
+ "padding": "56 56 48",
+ "headerIcon": 56,
+ "headerIconGlyph": 30,
+ "sectionGap": 40,
+ "labelToControl": 14,
+ "sliderHeight": 72,
+ "sliderRadius": 18,
+ "buttonHeight": 64,
+ "buttonRadius": 16,
+ "buttonGap": 12,
+ "listRowHeight": 54,
+ "focusRingOffset": 5
+ },
+ "media": {
+ "artSmall": 84,
+ "artLarge": 220,
+ "buttonSmall": 52,
+ "buttonPlay": 62,
+ "progress": 4
+ },
+ "focusRing": {
+ "width": 4,
+ "offset": 4,
+ "cardScale": 1.04
+ }
+ },
+ "shadow": {
+ "card": "0 2px 8px rgba(0, 0, 0, 0.25)",
+ "cardFocus": "0 16px 36px rgba(0, 0, 0, 0.50)",
+ "panel": "0 24px 60px rgba(0, 0, 0, 0.50)"
+ },
+ "motion": {
+ "focus": "120ms ease-out",
+ "panelOpen": "200ms ease-out, slide from right",
+ "bubbleFade": "1000ms after release"
+ },
+ "timing": {
+ "holdThresholdMs": 400,
+ "arrowRepeatDelayMs": 400,
+ "arrowRepeatMs": 150,
+ "stepDoubleAfterMs": 2000,
+ "sendDebounceMs": 200,
+ "confirmTimeoutMs": 5000,
+ "errorHintMs": 4000,
+ "volumePillMs": 1500,
+ "seekStepMs": 300,
+ "seekStepS": 10,
+ "seekStepFastS": 30,
+ "seekFastAfterMs": 2000,
+ "previousRestartAfterS": 3,
+ "volumeStepPct": 2
+ },
+ "airQuality": {
+ "metric": "CO2 ppm",
+ "goodBelow": 800,
+ "poorAbove": 1200
+ }
+}
\ No newline at end of file
diff --git a/design-system/ux-mechanics.md b/design-system/ux-mechanics.md
new file mode 100644
index 0000000..74e6f3e
--- /dev/null
+++ b/design-system/ux-mechanics.md
@@ -0,0 +1,465 @@
+# Luma Home — UX mechanics
+
+The behaviour contract for the Home Assistant tab. Every rule here is meant to be implemented
+exactly; where a number is given (timings, steps, sizes) it is the spec, not an example.
+Visual reference for every card and panel: `screens/`. Measurements and colours: `components.md`
+and `tokens.json`.
+
+Input is the LG remote only: **◀ ▶ ▲ ▼, OK, Back**. Viewing distance 5–6 m on a 65" TV, logical
+layout 1920 × 1080.
+
+---
+
+## Part A — Global rules
+
+### A1. Tap and hold on OK
+
+| | Rule |
+|---|---|
+| Tap | OK pressed and released in **under 0.4 s**. The action runs **on release**. |
+| Hold | OK still down at **0.4 s**. The hold action runs **at 0.4 s**, while the key is still down. The release that follows is ignored. |
+| Hold cue | From the moment OK goes down, a ring fills around the focused element's icon over 0.4 s (white stroke on a 18%-white track). Released early: the ring disappears and the tap runs. |
+| No hold action | If the focused element has no hold action, there is no ring and a long press behaves as a tap on release. |
+| Key repeat | OK auto-repeat events from the TV are ignored. Arrow auto-repeat is used where stated (sliders, volume). |
+
+Timing is measured from the key-down event to the key-up event as delivered by SDL-webOS
+(Wayland press/release). A tap never toggles anything on key-down, so a hold can never toggle by accident.
+
+### A2. Screen structure and focus order
+
+```
+Top nav (Start · Apps · Home · Settings)
+ ▼
+Room tabs (Home · Living Area · Kitchen · …)
+ ▼
+Card grid (4 × 3 or 3 × 3)
+```
+
+- **▲ from the top row of the grid** → room tabs (the active room's tab). **▲ from room tabs** → top nav.
+- **▼ from room tabs** → the card that was focused last in that room (first card the first time).
+- **◀ ▶ on room tabs** switches rooms immediately (no OK needed); the grid below changes, focus stays on the tabs.
+- **Back from a card** → room tabs. **Back from room tabs** → top nav. Back from top nav does nothing.
+- Focus memory: each room remembers its last focused card for the session.
+
+### A3. The grid
+
+- Every screen is a fixed grid: **4 columns × 3 rows** (cell 424 × 240) or, chosen per screen,
+ **3 columns × 3 rows** (cell 572 × 240). Gap 18 px, grid area 1752 × 756 at x 84, y 206.
+- Cards fill whole cells only: **1×1**, or **1×2** (one column, two rows tall, 498 px) for weather and media.
+- No scrolling inside a screen. A room with more cards than cells shows the first 12 (or 9); more
+ rooms or screens are added as tabs, not as scrolling.
+- Movement is spatial: ◀ ▶ ▲ ▼ go to the card whose cell is next in that direction. No wrap-around.
+- **1×2 cards:** entering from the side keeps the row you came from in memory; leaving sideways
+ returns to that row on the other side. ▼ from a 1×2 card goes to the cell below its bottom row.
+- Empty cells are skipped: the move continues in the same direction to the next card, or does nothing.
+
+### A4. Focus visuals
+
+- Focused card: **4 px white ring (96%)** with a **4 px dark gap** (`rgba(8,11,18,0.85)`) between
+ ring and card, card **scales to 1.04**, fill brightens, shadow deepens. 120 ms ease-out.
+- Inside a panel or the media card the ring sits on the focused control, not on the card.
+- Exactly one ring on screen at any time.
+
+### A5. Card anatomy (1×1)
+
+```
+┌───────────────────────────────────────┐
+│ (icon) Name Value │ ← top row: 48 px icon circle, name 19 px, value 20 px bold
+│ │
+│ ───────── middle band ─────────────── │ ← graph / level bar / colour strip / window / photo
+│ │
+│ Subtitle (context) │ ← 14 px, optional; context, never the state
+└───────────────────────────────────────┘
+```
+
+- **On**: icon circle and value take the card's accent colour, plus a soft radial glow top-left.
+- **Off**: grey icon circle (white 10%), value white 60%, no glow.
+- **Unavailable** (HA `unavailable`/`unknown`): card at 50% opacity, value "Offline", OK does nothing,
+ hint bar shows "Offline". Hold OK still opens the panel if the card has one (read-only).
+
+### A6. The hint bar
+
+Bottom-left of every screen, 14 px. It always describes the **focused** element:
+`OK · Hold OK · Back `.
+- Labels are verbs for the actual next result: "Turn on" when off, "Turn off" when on.
+- If there is no hold action, the "Hold OK" item is **not shown** (no "Details", no placeholder).
+- Media and panels add their arrow hints (e.g. "▲▼ Volume").
+
+### A7. Panels (Hold OK)
+
+- A panel opens on the **right side**, 620 × 1080, on top of the grid. The grid stays visible and dimmed behind it.
+- Focus moves into the panel on its **first section**. The card keeps a static (non-focused) highlight so you can see which card the panel belongs to.
+- Inside a panel: **▲ ▼ move between sections (rows)**, **◀ ▶ adjust or choose within the row**,
+ **OK selects / presses**, **Back closes** the panel and returns focus to the card.
+- Changes apply **live** while the panel is open — there is no Save. Slider changes are sent
+ **200 ms after the last press** (one call per burst); buttons and presets are sent at once.
+- If the entity becomes unavailable while the panel is open, the panel stays, controls go
+ disabled, and the header subtitle shows "Offline".
+- Sections that the entity doesn't support are **hidden**, not disabled (e.g. no airflow rows
+ on a radiator, no colour on a dimmable light).
+
+### A8. Sliders inside panels
+
+- ◀ ▶ change by the stated step. Holding an arrow repeats every **150 ms** after an initial **400 ms**;
+ after 2 s of holding the step doubles.
+- The value label updates on every step; HA receives the value 200 ms after the last step.
+- Values clamp at the ends; no wrap-around.
+
+### A9. State updates and errors
+
+- **Optimistic:** the card shows the new state the moment the action is sent.
+- **Confirmation:** the real HA state replaces it when it arrives. If no matching state arrives within **5 s**,
+ the card reverts and the hint bar shows "Couldn't reach " for 4 s.
+- HA state changes from elsewhere (app, automation) update cards and open panels live, except a
+ control currently being adjusted (it keeps the local value until 1.5 s after the last press).
+
+---
+
+## Part B — Cards
+
+Each card: what it shows, every key, the panel, states, hint bar and Home Assistant calls.
+
+### B1. Light — on/off
+
+For lights that only switch (no brightness).
+
+| | |
+|---|---|
+| Size | 1×1 |
+| Shows | Name, value **On / Off**, optional subtitle. No middle band. Accent gold `#F5B85A`. |
+| OK (tap) | Toggle. |
+| Hold OK | Nothing (no panel, no ring). |
+| Hint bar | `OK Turn on` / `OK Turn off` |
+| HA | `light.toggle` (or `light.turn_on` / `light.turn_off`) |
+
+### B2. Light — dimmable (no colour)
+
+For lights with brightness only (e.g. Ceiling Lights).
+
+| | |
+|---|---|
+| Size | 1×1 |
+| Shows | Value = brightness % or **Off**. Middle band: brightness bar (12 px) in the accent. |
+| OK (tap) | On / off. Turning on restores the **last brightness**. |
+| Hold OK | Brightness panel. |
+| Hint bar | `OK Turn on · Hold OK Brightness & presets` |
+
+**Panel — sections top to bottom**
+1. **Brightness** — slider 1–100 %, ◀ ▶ **10 %** steps (A8). Going below 10 % with ◀ stops at 1 %; ◀ again turns off.
+2. **Presets** — Off · 25 % · 50 % · 100 %. ◀ ▶ choose, OK applies.
+
+OK on the Brightness row toggles on/off. Back closes.
+
+HA: `light.turn_on` with `brightness_pct`; `light.turn_off`.
+
+### B3. Light — colour (white + RGB)
+
+| | |
+|---|---|
+| Size | 1×1 |
+| Shows | Value = brightness %. Middle band: brightness bar **in the light's current colour**. Accent, glow and bar follow the live colour (warm white → gold). Subtitle: "White · 2700 K" or "Colour". |
+| OK (tap) | On / off at the last colour and brightness. |
+| Hold OK | Brightness & colour panel. |
+| Hint bar | `OK Turn on · Hold OK Brightness & colour` |
+
+**Panel**
+1. **Brightness** — as B2.
+2. **Light type** — two buttons **White · Colour**. ◀ ▶ + OK switches. White restores the last
+ colour temperature, Colour restores the last hue.
+3. **White temperature** (White mode) — slider, ◀ ▶ **250 K** steps within the light's min–max
+ (typically 2700–6500 K). Label "2700 K · warm" / "4000 K · neutral" / "6500 K · cool".
+ **or Hue** (Colour mode) — ◀ ▶ **15°** steps around the colour wheel, full saturation.
+4. **Swatches** — White mode: 2700 · 3000 · 4000 · 5000 · 6500 K. Colour mode: 8 colours
+ (red, orange, yellow, green, blue, violet, pink, white). OK applies.
+
+HA: `light.turn_on` with `brightness_pct`, `color_temp_kelvin` or `hs_color`.
+
+### B4. LED strips and TV lights (with effects)
+
+| | |
+|---|---|
+| Size | 1×1 |
+| Shows | Value = brightness % or Off. Middle band: 34 px strip showing the current colour, or the effect's gradient. Subtitle: "Effect: Aurora", "White · 3000 K" or "Colour". |
+| OK (tap) | On / off (restores last colour or effect). |
+| Hold OK | Colours & effects panel. |
+| Hint bar | `OK Turn on · Hold OK Colours & effects` |
+
+**Panel**
+1. **Brightness** — as B2.
+2. **Light** — White · Colour (as B3). Switching modes swaps the presets in row 4.
+3. **Temperature** (White) or **Hue** (Colour) — as B3.
+4. **White presets** (2700 · 3000 · 4000 · 5000 · 6500 K) **or Colour presets** (8 swatches).
+5. **Effects** — up to **6 pinned effects** as buttons, then **"All effects · N ›"**.
+ OK on an effect plays it (the button shows "Playing"). OK on "All effects" opens the list.
+
+**All effects list** (replaces the panel content, same position)
+- Two groups: **Pinned** (in pin order, numbered) then **All effects A–Z**.
+- ▲ ▼ move, the list scrolls to keep focus visible. **OK plays** the effect.
+ **Hold OK pins / unpins** it (star fills gold).
+- Pins are unlimited. **The panel shows the first 6 pins.** A 7th or later pin stays in the
+ Pinned group at the top of this list, tagged "Not on panel"; nothing changes on the panel.
+ Unpinning one of the first 6 moves the next pin up onto the panel.
+- Back returns to the panel with focus on the Effects row.
+- Pins are stored per light in local preferences.
+
+HA: `light.turn_on` with `effect` (list from the entity's `effect_list`), `brightness_pct`, `hs_color`, `color_temp_kelvin`.
+
+### B5. Air conditioner (Gree AC)
+
+| | |
+|---|---|
+| Size | 1×1 |
+| Shows | Value = target temperature ("25°"). Middle band: 24 h room-temperature graph (grey when off). Subtitle: "Heat · 26° inside". Accent by mode: heat `#F06A4A`, cool blue. |
+| OK (tap) | On / off; turning on resumes the **last mode**. |
+| Hold OK | AC panel. |
+| Hint bar | `OK Turn on · Hold OK Mode, fan & airflow` |
+
+**Panel — sections top to bottom**
+1. **Target** — ◀ ▶ by the entity's step (Gree: 1°), within its min–max.
+2. **Mode** — icon buttons: Auto · Cool · Dry · Fan · Heat. OK selects.
+3. **Fan speed · auto** — preset pills **Auto · Quiet · Turbo**. OK selects.
+4. **Fan speed · manual** — 5-step slider Low · Medium-low · Medium · Medium-high · High.
+ ◀ ▶ moves one step and selects manual. Selecting a step clears the auto preset;
+ selecting an auto preset greys the slider (still focusable).
+5. **Up–down airflow** — icon-only: **Swing** + 5 fixed positions.
+6. **Left–right airflow** — icon-only: **Swing** + 5 fixed positions.
+ Row label shows the current value ("Fixed · position 2", "Swing").
+
+Radiators and wall thermostats have their own card (B5b).
+Gree extra toggles (Light, X-Fan, Health, Sleep, SE) are **not** included.
+
+HA: `climate.turn_on/turn_off`, `climate.set_temperature`, `climate.set_hvac_mode`,
+`climate.set_fan_mode`, `climate.set_swing_mode` (and the Gree integration's swing-position
+entities for fixed positions).
+
+### B5b. Heating — radiator valve (TRV) and wall thermostat
+
+One card type with two versions, picked by the device. Both are `climate` entities with modes
+**off / heat** only.
+
+| | |
+|---|---|
+| Size | 1×1 |
+| Shows | Value = target temperature, or **Off**. Middle band: 24 h room-temperature graph. Subtitle: what it is doing + room temperature — "Heating · 22.5° inside", "Idle · 23° inside" (target reached), or "22.5° inside" when off. Accent heat `#F06A4A`. |
+| Warnings | Shown in the subtitle in **amber** `#F2B84B`, 600 weight, replacing the normal text: "Window open · paused" (value shows the setback temperature), "Battery low". |
+| Unavailable | Card at 50 %, value "Offline", subtitle "Last seen