Files
ledgrab/docs/custom-card-icon-mockup.html
T
alexei.dolgolyov 49ddabbc36 feat(ui): customisable card icon plate for devices
A user-chosen icon ("mouse", "motherboard", "keyboard"…) renders as a
44x44 instrument-panel face plate at the leading edge of .mod-head on
device cards. Optional per-card; null hides the plate and reverts to
the existing badge-only head.

- Storage/schema: new icon, icon_color fields on Device + DeviceUpdate /
  DeviceResponse. SQLite stores entities as a JSON blob, so no migration
  is needed; from_dict defaults handle existing rows.
- Curated 47-icon library across six categories (Hardware / Lighting /
  Rooms / Media / Signal / Ambience), reusing the existing Lucide path
  module; adds circuit-board, bed, armchair, leaf paths.
- mod-card.ts: ModHeadOpts gains iconHtml / iconColor / iconAttrs;
  ModMenuItemOpts gains optional dataAttrs. The plate is rendered when
  iconHtml is supplied; otherwise no layout change.
- Picker modal (icon-picker.html + features/icon-picker.ts): live
  preview, search, six category tabs, recent strip, channel-color
  override toggle. Wired through document-level click delegation on
  [data-icon-picker-trigger="<deviceId>"] — no window globals, no
  inline onclick string. Sets the precedent for migrating other card
  actions off window in a follow-up.
- en/ru/zh locales for picker UI + categories.

Includes a docs/ mockup that's the source-of-truth for the design.
2026-05-03 15:08:17 +03:00

1604 lines
83 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>LedGrab · Custom Card Icon · Proposal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@700;800;900&family=JetBrains+Mono:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* =====================================================================
CUSTOM CARD ICON — proposal layered onto the existing .module / .mod-*
instrument-panel language. A square "icon plate" sits at the leading
edge of the head row, tinted by the channel accent. Click to open the
picker; behaviour falls through transparently for cards that don't set
an icon (current default).
===================================================================== */
:root{
--primary-color:#4CAF50;
--primary-text:#66bb6a;
--primary-contrast:#fff;
--font-display:'Big Shoulders Display','Orbitron',sans-serif;
--font-body:'Manrope',-apple-system,BlinkMacSystemFont,sans-serif;
--font-mono:'JetBrains Mono','Cascadia Code',ui-monospace,monospace;
--lux-r-sm:3px;
--lux-r-md:6px;
--lux-hairline:1px;
--ch-signal:#4CAF50;
--ch-cyan:#00d8ff;
--ch-magenta:#ff4ade;
--ch-amber:#ffb800;
--ch-coral:#ff5e5e;
--ch-violet:#8b7eff;
--duration:.22s;
--ease:cubic-bezier(.16,1,.3,1);
}
[data-theme="dark"]{
--bg-page:#000;
--lux-bg-0:#000;
--lux-bg-1:#0e1014;
--lux-bg-2:#15181d;
--lux-bg-3:#1c2027;
--lux-line:#232831;
--lux-line-bold:#2e3440;
--lux-ink:#e6ebf2;
--lux-ink-dim:#8b95a5;
--lux-ink-mute:#5b6473;
--lux-ink-faint:#3a414c;
--shadow-rack:0 1px 0 rgba(255,255,255,.03), 0 8px 24px rgba(0,0,0,.5);
color-scheme:dark;
}
[data-theme="light"]{
--bg-page:#f0f1f4;
--lux-bg-0:#fff;
--lux-bg-1:#f6f8fb;
--lux-bg-2:#eef1f5;
--lux-bg-3:#e4e8ee;
--lux-line:#dee3ea;
--lux-line-bold:#c4ccd6;
--lux-ink:#0f1419;
--lux-ink-dim:#4c5866;
--lux-ink-mute:#6b7684;
--lux-ink-faint:#a5afbc;
--shadow-rack:0 1px 0 rgba(255,255,255,.6), 0 6px 18px rgba(15,20,25,.08);
--primary-color:#2e7d32;
--ch-signal:#2e7d32;
--ch-cyan:#006b88;
--ch-magenta:#b01a99;
--ch-amber:#a56a00;
--ch-coral:#d8392e;
--ch-violet:#5b4fd0;
color-scheme:light;
}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{
background:var(--bg-page);color:var(--lux-ink);
font-family:var(--font-body);min-height:100vh;
}
body{
padding:48px 32px 80px;-webkit-font-smoothing:antialiased;
background-image:
radial-gradient(circle at 88% -10%, color-mix(in srgb, var(--ch-signal) 9%, transparent), transparent 45%),
radial-gradient(circle at -10% 30%, color-mix(in srgb, var(--ch-violet) 7%, transparent), transparent 50%),
linear-gradient(var(--lux-line) 1px, transparent 1px),
linear-gradient(90deg, var(--lux-line) 1px, transparent 1px);
background-size: auto, auto, 80px 80px, 80px 80px;
background-blend-mode: normal, normal, multiply, multiply;
}
[data-theme="dark"] body::before{
content:'';position:fixed;inset:0;pointer-events:none;z-index:0;
background:radial-gradient(ellipse 80% 60% at 50% -10%, rgba(76,175,80,.06), transparent);
}
.page{max-width:1360px;margin:0 auto;position:relative;z-index:1;}
/* =================== Page chrome =================== */
.page-head{
display:flex;align-items:flex-end;justify-content:space-between;gap:24px;
padding-bottom:28px;margin-bottom:36px;
border-bottom:1px solid var(--lux-line);
position:relative;
}
.page-head::after{
content:'';position:absolute;left:0;bottom:-1px;width:160px;height:2px;
background:linear-gradient(90deg,var(--ch-signal),transparent);
}
.eyebrow{
font-family:var(--font-mono);font-size:.7rem;letter-spacing:.32em;
text-transform:uppercase;color:var(--lux-ink-mute);margin-bottom:10px;
display:flex;align-items:center;gap:10px;
}
.eyebrow::before{
content:'';width:6px;height:6px;border-radius:50%;background:var(--ch-signal);
box-shadow:0 0 0 3px color-mix(in srgb, var(--ch-signal) 20%, transparent);
animation:pulse 2s ease-in-out infinite;
}
@keyframes pulse{50%{box-shadow:0 0 0 6px color-mix(in srgb, var(--ch-signal) 0%, transparent);}}
h1{
font-family:var(--font-display);
font-size:clamp(2.4rem,5vw,3.8rem);font-weight:900;
letter-spacing:-.02em;line-height:.92;color:var(--lux-ink);
}
h1 em{font-style:normal;color:var(--ch-signal);}
.lede{
color:var(--lux-ink-dim);max-width:58ch;font-size:.95rem;line-height:1.55;
margin-top:14px;
}
.lede code{
font-family:var(--font-mono);font-size:.78rem;
background:var(--lux-bg-0);border:1px solid var(--lux-line);
padding:1px 5px;border-radius:3px;color:var(--ch-cyan);
}
.theme-toggle{
display:inline-flex;align-items:center;gap:0;font-family:var(--font-mono);
font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
border:1px solid var(--lux-line-bold);border-radius:999px;
background:var(--lux-bg-1);overflow:hidden;
}
.theme-toggle button{
appearance:none;background:none;border:none;color:var(--lux-ink-mute);
padding:8px 16px;cursor:pointer;font:inherit;letter-spacing:inherit;
transition:color .2s,background .2s;
}
.theme-toggle button.is-active{background:var(--lux-ink);color:var(--lux-bg-1);}
/* =================== Section heading =================== */
.section-title{
font-family:var(--font-mono);font-size:.72rem;letter-spacing:.3em;
text-transform:uppercase;color:var(--lux-ink-mute);
margin:64px 0 22px;display:flex;align-items:center;gap:14px;
}
.section-title::after{content:'';flex:1;height:1px;background:var(--lux-line);}
.section-title .num{
font-family:var(--font-display);font-size:1.6rem;font-weight:800;
color:var(--lux-ink);letter-spacing:0;
border-right:1px solid var(--lux-line);padding-right:14px;
}
.tag{
display:inline-flex;align-items:center;gap:4px;
font-family:var(--font-mono);font-size:.6rem;font-weight:600;
letter-spacing:.16em;text-transform:uppercase;
color:var(--ch-signal);
border:1px solid color-mix(in srgb, var(--ch-signal) 30%, transparent);
background:color-mix(in srgb, var(--ch-signal) 10%, transparent);
padding:2px 7px;border-radius:3px;
}
/* =================== Module / card vocabulary (verbatim from project) === */
.grid{
display:grid;
grid-template-columns:repeat(auto-fill,minmax(min(380px,100%),1fr));
gap:14px;
}
.module{
--ch:var(--ch-signal);
position:relative;overflow:hidden;
display:flex;flex-direction:column;gap:14px;
padding:16px 18px 14px 22px;
background:var(--lux-bg-1);
border:var(--lux-hairline) solid var(--lux-line);
border-radius:var(--lux-r-md);
transition:box-shadow .2s ease, transform .2s ease, border-color .2s ease;
}
.module::before{
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
background:var(--ch);opacity:.6;
box-shadow:0 0 8px color-mix(in srgb, var(--ch) 40%, transparent);
transition:opacity .2s ease, box-shadow .2s ease, width .2s ease;
}
.module::after{
content:'';position:absolute;top:8px;right:8px;width:12px;height:12px;
border-top:var(--lux-hairline) solid var(--lux-line-bold);
border-right:var(--lux-hairline) solid var(--lux-line-bold);
pointer-events:none;opacity:.7;
}
.module:hover{
box-shadow:var(--shadow-rack);
border-color:var(--lux-line-bold);
transform:translateY(-1px);
}
.module:hover::before{
opacity:1;
box-shadow:0 0 14px color-mix(in srgb, var(--ch) 70%, transparent);
}
.module.is-running{
border-color:color-mix(in srgb, var(--ch) 32%, var(--lux-line));
box-shadow:0 0 0 1px color-mix(in srgb, var(--ch) 18%, transparent),
0 6px 20px rgba(0,0,0,.25);
}
.module.is-running::before{
opacity:1;width:4px;
box-shadow:0 0 14px color-mix(in srgb, var(--ch) 70%, transparent),
0 0 4px color-mix(in srgb, var(--ch) 90%, transparent);
}
.module[data-ch="signal"] {--ch:var(--ch-signal);}
.module[data-ch="cyan"] {--ch:var(--ch-cyan);}
.module[data-ch="magenta"] {--ch:var(--ch-magenta);}
.module[data-ch="amber"] {--ch:var(--ch-amber);}
.module[data-ch="coral"] {--ch:var(--ch-coral);}
.module[data-ch="violet"] {--ch:var(--ch-violet);}
.mod-head{display:flex;align-items:flex-start;gap:12px;}
.mod-id{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1;}
.mod-badge{
display:inline-flex;align-items:center;gap:6px;align-self:flex-start;
font-family:var(--font-mono);font-size:.55rem;font-weight:600;
letter-spacing:.2em;text-transform:uppercase;color:var(--ch);
padding:2px 6px;
border:var(--lux-hairline) solid color-mix(in srgb, var(--ch) 35%, var(--lux-line));
border-radius:3px;
background:color-mix(in srgb, var(--ch) 8%, transparent);
line-height:1.4;white-space:nowrap;
}
.mod-name{
font-family:var(--font-body);
font-size:1.05rem;font-weight:700;letter-spacing:-.01em;
color:var(--lux-ink);
display:flex;align-items:center;gap:6px;
min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.mod-meta{
font-family:var(--font-mono);font-size:.66rem;letter-spacing:.06em;
color:var(--lux-ink-mute);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.mod-leds{
display:flex;align-items:center;gap:4px;padding:5px 7px;
background:var(--lux-bg-0);
border:var(--lux-hairline) solid var(--lux-line);
border-radius:var(--lux-r-sm);flex-shrink:0;
}
.mod-leds .led{
width:6px;height:6px;border-radius:50%;
background:var(--lux-ink-faint);
box-shadow:inset 0 0 0 1px rgba(0,0,0,.5);
}
.mod-leds .led.on{
background:var(--ch);
box-shadow:0 0 6px color-mix(in srgb, var(--ch) 80%, transparent);
}
.mod-leds .led.blink{animation:ledBlink 1.2s ease-in-out infinite;}
.mod-leds .led.blink:nth-child(2){animation-delay:.2s;}
.mod-leds .led.blink:nth-child(3){animation-delay:.4s;}
@keyframes ledBlink{0%,100%{opacity:1;}50%{opacity:.28;}}
.mod-leds .led.fault{
background:var(--ch-coral);
box-shadow:0 0 8px color-mix(in srgb, var(--ch-coral) 70%, transparent);
}
.mod-metrics{
display:grid;grid-template-columns:1.2fr 1fr 1fr;
background:var(--lux-bg-0);
border:var(--lux-hairline) solid var(--lux-line);
border-radius:var(--lux-r-sm);overflow:hidden;
}
.mod-metric{
padding:9px 12px 10px;
border-right:var(--lux-hairline) solid var(--lux-line);
display:flex;flex-direction:column;gap:3px;min-width:0;
}
.mod-metric:last-child{border-right:none;}
.mod-metric .k{
font-family:var(--font-mono);font-size:.55rem;font-weight:600;
letter-spacing:.18em;text-transform:uppercase;
color:var(--lux-ink-mute);
}
.mod-metric .v{
font-family:var(--font-display);
font-size:1.95rem;font-weight:800;line-height:1;color:var(--lux-ink);
font-variant-numeric:tabular-nums;letter-spacing:-.01em;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.mod-metric .v.signal{color:var(--ch);}
.mod-metric .v small{
font-family:var(--font-mono);font-size:.65rem;font-weight:500;
color:var(--lux-ink-mute);letter-spacing:.08em;margin-left:3px;
}
.mod-fader{
display:flex;align-items:center;gap:10px;padding:7px 10px;
background:var(--lux-bg-0);
border:var(--lux-hairline) solid var(--lux-line);
border-radius:var(--lux-r-sm);
}
.mod-fader__k{
font-family:var(--font-mono);font-size:.55rem;
letter-spacing:.2em;text-transform:uppercase;
color:var(--lux-ink-mute);min-width:42px;
}
.mod-fader__track{
flex:1;position:relative;height:5px;border-radius:99px;
background:color-mix(in srgb, var(--ch) 12%, var(--lux-bg-3));
overflow:hidden;box-shadow:inset 0 1px 2px rgba(0,0,0,.4);
}
.mod-fader__fill{
position:absolute;left:0;top:0;bottom:0;
background:linear-gradient(90deg,
color-mix(in srgb, var(--ch) 50%, transparent), var(--ch));
box-shadow:0 0 8px color-mix(in srgb, var(--ch) 60%, transparent);
}
.mod-fader__v{
font-family:var(--font-mono);font-size:.78rem;font-weight:700;
color:var(--lux-ink);min-width:34px;text-align:right;
font-variant-numeric:tabular-nums;
}
.mod-foot{display:flex;align-items:center;gap:8px;padding-top:2px;}
.mod-patch{
display:flex;align-items:center;gap:6px;
font-family:var(--font-mono);font-size:.6rem;font-weight:600;
letter-spacing:.1em;text-transform:uppercase;
color:var(--lux-ink-mute);margin-right:auto;min-width:0;
}
.mod-patch .patch-dot{
width:8px;height:8px;border-radius:50%;
background:var(--lux-bg-0);
border:var(--lux-hairline) solid color-mix(in srgb, var(--ch) 55%, var(--lux-line-bold));
flex-shrink:0;position:relative;
}
.mod-patch .patch-dot.is-live::after{
content:'';position:absolute;inset:1px;border-radius:50%;
background:var(--ch);box-shadow:0 0 6px var(--ch);
animation:pulse 2s ease-in-out infinite;
}
.mod-btn{
display:inline-flex;align-items:center;justify-content:center;gap:6px;
font-family:var(--font-mono);font-size:.7rem;font-weight:600;
letter-spacing:.08em;text-transform:uppercase;
padding:7px 14px;flex:0 0 auto;
border:var(--lux-hairline) solid var(--lux-line-bold);
border-radius:var(--lux-r-sm);
background:var(--lux-bg-2);color:var(--lux-ink-dim);
cursor:pointer;transition:all .15s ease;
}
.mod-btn:hover{
color:var(--lux-ink);
border-color:color-mix(in srgb, var(--ch) 40%, var(--lux-line-bold));
background:var(--lux-bg-3);
}
.mod-btn-stop{
color:var(--ch-coral);
border-color:color-mix(in srgb, var(--ch-coral) 40%, transparent);
}
.mod-btn-go{
background:var(--ch);color:#fff;border-color:var(--ch);
box-shadow:0 0 14px color-mix(in srgb, var(--ch) 35%, transparent);
}
.mod-btn svg{width:12px;height:12px;}
.mod-btn-icon{padding:7px 9px;}
.mod-btn-icon svg{width:14px;height:14px;}
/* =====================================================================
THE PROPOSAL — .mod-icon (the icon plate)
A 52x52 panel-mount tile that leads the head row. Channel-tinted.
Click → opens picker. Empty → renders a placeholder "+" plate so the
slot stays visually consistent (and discoverable).
===================================================================== */
.mod-head--with-icon{align-items:stretch;}
.mod-icon{
--plate-size:52px;
flex:0 0 var(--plate-size);
width:var(--plate-size);height:var(--plate-size);
position:relative;
display:inline-flex;align-items:center;justify-content:center;
background:linear-gradient(180deg,
color-mix(in srgb, var(--ch) 10%, var(--lux-bg-0)) 0%,
var(--lux-bg-0) 100%);
border:var(--lux-hairline) solid color-mix(in srgb, var(--ch) 28%, var(--lux-line));
border-radius:var(--lux-r-sm);
color:var(--ch);
cursor:pointer;
transition:transform .18s var(--ease),
border-color .18s var(--ease),
box-shadow .18s var(--ease),
background .18s var(--ease);
box-shadow:
inset 0 1px 0 color-mix(in srgb, var(--ch) 14%, transparent),
inset 0 -8px 14px color-mix(in srgb, var(--ch) 6%, transparent),
0 0 0 0 color-mix(in srgb, var(--ch) 35%, transparent);
overflow:hidden;
isolation:isolate;
}
/* Inner corner-bracket detail: ties to the card's silkscreen language */
.mod-icon::before{
content:'';position:absolute;top:4px;right:4px;width:7px;height:7px;
border-top:1px solid color-mix(in srgb, var(--ch) 55%, var(--lux-line));
border-right:1px solid color-mix(in srgb, var(--ch) 55%, var(--lux-line));
opacity:.7;pointer-events:none;
}
/* Subtle scanline texture so the plate reads as instrument-grade, not flat */
.mod-icon::after{
content:'';position:absolute;inset:0;pointer-events:none;
background:repeating-linear-gradient(180deg,
rgba(255,255,255,.02) 0 1px,
transparent 1px 3px);
mix-blend-mode:overlay;opacity:.5;
}
.mod-icon svg{
width:28px;height:28px;
stroke:currentColor;stroke-width:1.6;
fill:none;stroke-linecap:round;stroke-linejoin:round;
filter:drop-shadow(0 0 6px color-mix(in srgb, var(--ch) 35%, transparent));
z-index:1;transition:transform .25s var(--ease);
}
.mod-icon:hover{
transform:translateY(-1px);
border-color:color-mix(in srgb, var(--ch) 60%, var(--lux-line));
box-shadow:
inset 0 1px 0 color-mix(in srgb, var(--ch) 24%, transparent),
inset 0 -10px 18px color-mix(in srgb, var(--ch) 10%, transparent),
0 0 0 3px color-mix(in srgb, var(--ch) 18%, transparent),
0 6px 16px color-mix(in srgb, var(--ch) 22%, transparent);
}
.mod-icon:hover svg{transform:scale(1.05);}
.mod-icon:focus-visible{
outline:none;
box-shadow:0 0 0 2px color-mix(in srgb, var(--ch) 60%, transparent);
}
/* "Edit" pencil hint — appears on hover, sits inside the bottom-right */
.mod-icon__edit{
position:absolute;bottom:3px;right:3px;
width:14px;height:14px;border-radius:2px;
background:color-mix(in srgb, var(--ch) 18%, var(--lux-bg-0));
border:1px solid color-mix(in srgb, var(--ch) 50%, transparent);
display:flex;align-items:center;justify-content:center;
color:var(--ch);opacity:0;transform:translateY(2px);
transition:opacity .15s, transform .15s;pointer-events:none;
}
.mod-icon__edit svg{
width:9px;height:9px;stroke-width:2;filter:none;
}
.mod-icon:hover .mod-icon__edit,
.mod-icon:focus-visible .mod-icon__edit{
opacity:1;transform:translateY(0);
}
/* Running cards: the plate "breathes" in sync with the live indicator */
.module.is-running .mod-icon{
animation:platePulse 2.6s ease-in-out infinite;
}
@keyframes platePulse{
0%,100%{
box-shadow:
inset 0 1px 0 color-mix(in srgb, var(--ch) 14%, transparent),
inset 0 -8px 14px color-mix(in srgb, var(--ch) 6%, transparent),
0 0 0 0 color-mix(in srgb, var(--ch) 35%, transparent);
}
50%{
box-shadow:
inset 0 1px 0 color-mix(in srgb, var(--ch) 22%, transparent),
inset 0 -8px 18px color-mix(in srgb, var(--ch) 12%, transparent),
0 0 0 4px color-mix(in srgb, var(--ch) 12%, transparent),
0 0 16px color-mix(in srgb, var(--ch) 30%, transparent);
}
}
/* Offline / fault states */
.module[data-state="offline"] .mod-icon{
color:var(--lux-ink-mute);
border-color:var(--lux-line);
background:var(--lux-bg-0);
}
.module[data-state="offline"] .mod-icon svg{
filter:none;opacity:.55;
}
.module[data-state="fault"] .mod-icon{
color:var(--ch-coral);
border-color:color-mix(in srgb, var(--ch-coral) 45%, var(--lux-line));
background:linear-gradient(180deg,
color-mix(in srgb, var(--ch-coral) 14%, var(--lux-bg-0)) 0%,
var(--lux-bg-0) 100%);
}
/* Empty / placeholder plate — kept visible so the slot is discoverable.
Styled as a dashed outline with a quiet "+" glyph. */
.mod-icon.is-empty{
background:transparent;
border-style:dashed;
border-color:var(--lux-line-bold);
color:var(--lux-ink-mute);
box-shadow:none;
}
.mod-icon.is-empty::before{display:none;}
.mod-icon.is-empty::after{display:none;}
.mod-icon.is-empty svg{filter:none;opacity:.7;width:18px;height:18px;}
.mod-icon.is-empty:hover{
border-color:color-mix(in srgb, var(--ch) 60%, var(--lux-line-bold));
color:var(--ch);
background:color-mix(in srgb, var(--ch) 5%, transparent);
}
/* =====================================================================
Anatomy section — exploded view of the plate with annotations
===================================================================== */
.anatomy{
display:grid;
grid-template-columns:minmax(280px,420px) 1fr;
gap:32px;align-items:center;
padding:36px 32px;
background:linear-gradient(180deg,var(--lux-bg-1),var(--lux-bg-0) 140%);
border:1px solid var(--lux-line);
border-radius:var(--lux-r-md);
position:relative;overflow:hidden;
}
.anatomy::before{
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
background:linear-gradient(180deg,var(--ch-cyan),var(--ch-violet));
opacity:.7;
}
.anatomy-stage{
display:flex;justify-content:center;align-items:center;
aspect-ratio:1;
background:
radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--ch-signal) 14%, transparent), transparent 60%),
repeating-linear-gradient(0deg, var(--lux-line) 0 1px, transparent 1px 28px),
repeating-linear-gradient(90deg, var(--lux-line) 0 1px, transparent 1px 28px),
var(--lux-bg-0);
border:1px solid var(--lux-line);
border-radius:var(--lux-r-sm);
position:relative;
}
.anatomy-stage .mod-icon{
--plate-size:160px;
cursor:default;animation:platePulse 3.4s ease-in-out infinite;
}
.anatomy-stage .mod-icon svg{width:88px;height:88px;stroke-width:1.4;}
.anatomy-stage .mod-icon::before{width:18px;height:18px;top:8px;right:8px;}
/* Annotation lines */
.anno{
position:absolute;
font-family:var(--font-mono);font-size:.6rem;
letter-spacing:.16em;text-transform:uppercase;
color:var(--lux-ink-dim);
display:flex;align-items:center;gap:8px;
white-space:nowrap;
}
.anno::before{
content:'';width:24px;height:1px;background:var(--lux-ink-mute);
}
.anno-1{top:18%;left:calc(50% + 92px);}
.anno-2{top:60%;left:calc(50% + 92px);}
.anno-3{bottom:18%;right:calc(50% + 92px);flex-direction:row-reverse;}
.anno-3::before{margin-left:0;}
.anno-4{top:38%;right:calc(50% + 92px);flex-direction:row-reverse;}
.anno-4::before{margin-left:0;}
.anno b{font-weight:700;color:var(--lux-ink);letter-spacing:.16em;}
.anatomy-copy h3{
font-family:var(--font-display);font-size:2rem;font-weight:800;
letter-spacing:-.01em;line-height:1;margin-bottom:14px;
}
.anatomy-copy h3 em{font-style:normal;color:var(--ch-cyan);}
.anatomy-copy p{
color:var(--lux-ink-dim);line-height:1.55;font-size:.95rem;
margin-bottom:14px;max-width:54ch;
}
.spec-list{
list-style:none;display:flex;flex-direction:column;gap:10px;margin-top:18px;
}
.spec-list li{
display:grid;grid-template-columns:96px 1fr;gap:14px;
font-size:.86rem;color:var(--lux-ink-dim);line-height:1.5;
}
.spec-list .k{
font-family:var(--font-mono);font-size:.62rem;
letter-spacing:.18em;text-transform:uppercase;
color:var(--ch-signal);align-self:start;padding-top:2px;
border-left:2px solid color-mix(in srgb, var(--ch-signal) 50%, transparent);
padding-left:10px;
}
.spec-list strong{color:var(--lux-ink);font-weight:700;}
.spec-list code{
font-family:var(--font-mono);font-size:.78rem;
background:var(--lux-bg-0);border:1px solid var(--lux-line);
padding:1px 5px;border-radius:3px;color:var(--ch-cyan);
}
/* =====================================================================
Variants strip — show the plate in idle / running / offline / empty
===================================================================== */
.variants{
display:grid;grid-template-columns:repeat(4, 1fr);gap:14px;
margin-bottom:24px;
}
.variant{
padding:18px;
background:var(--lux-bg-1);
border:1px solid var(--lux-line);
border-radius:var(--lux-r-md);
display:flex;flex-direction:column;align-items:center;gap:14px;
position:relative;overflow:hidden;
}
.variant::before{
content:'';position:absolute;left:0;top:0;bottom:0;width:2px;
background:var(--ch);opacity:.5;
}
.variant[data-ch="signal"]{--ch:var(--ch-signal);}
.variant[data-ch="cyan"]{--ch:var(--ch-cyan);}
.variant[data-ch="amber"]{--ch:var(--ch-amber);}
.variant[data-ch="coral"]{--ch:var(--ch-coral);}
.variant .label{
font-family:var(--font-mono);font-size:.6rem;
letter-spacing:.22em;text-transform:uppercase;
color:var(--lux-ink-mute);
}
.variant .label b{color:var(--ch);font-weight:700;}
.variant .stage{
--plate-size:84px;
width:var(--plate-size);height:var(--plate-size);
}
.variant .stage .mod-icon{--plate-size:84px;}
.variant .stage .mod-icon svg{width:42px;height:42px;}
.variant .desc{
font-size:.78rem;color:var(--lux-ink-dim);text-align:center;line-height:1.5;
}
/* =====================================================================
Picker modal — shown inline as a "live" demo (not really a backdrop;
it sits in the page flow with a dimmed shell)
===================================================================== */
.picker-stage{
position:relative;padding:48px 24px;
background:
radial-gradient(ellipse at 50% 50%, color-mix(in srgb, var(--ch-violet) 12%, transparent), transparent 60%),
var(--lux-bg-0);
border:1px solid var(--lux-line);
border-radius:var(--lux-r-md);
overflow:hidden;
}
.picker-stage::before,
.picker-stage::after{
content:'';position:absolute;width:140px;height:140px;
border:1px solid color-mix(in srgb, var(--ch-violet) 40%, var(--lux-line));
border-radius:50%;opacity:.4;pointer-events:none;
}
.picker-stage::before{top:-40px;left:-40px;}
.picker-stage::after{bottom:-60px;right:-60px;border-color:color-mix(in srgb, var(--ch-cyan) 40%, var(--lux-line));}
.picker{
width:min(720px,100%);margin:0 auto;
background:var(--lux-bg-1);
border:1px solid var(--lux-line-bold);
border-radius:var(--lux-r-md);
box-shadow:
0 1px 0 rgba(255,255,255,.03),
0 24px 48px rgba(0,0,0,.55);
overflow:hidden;position:relative;
--ch:var(--ch-cyan);
}
.picker::before{
content:'';position:absolute;left:0;top:0;right:0;height:2px;
background:linear-gradient(90deg,var(--ch-cyan),var(--ch-violet),var(--ch-magenta));
opacity:.85;
}
.picker-head{
display:flex;align-items:center;gap:14px;
padding:18px 22px 14px;border-bottom:1px solid var(--lux-line);
}
.picker-head .preview{
--plate-size:64px;
flex:0 0 var(--plate-size);
}
.picker-head .preview .mod-icon{--plate-size:64px;cursor:default;}
.picker-head .preview .mod-icon svg{width:34px;height:34px;}
.picker-head .meta{flex:1;min-width:0;}
.picker-head .eyebrow-mini{
font-family:var(--font-mono);font-size:.58rem;font-weight:600;
letter-spacing:.24em;text-transform:uppercase;
color:var(--ch-cyan);margin-bottom:4px;
}
.picker-head h2{
font-family:var(--font-display);font-size:1.45rem;font-weight:800;
letter-spacing:-.005em;line-height:1.05;color:var(--lux-ink);
}
.picker-head .sub{
font-family:var(--font-mono);font-size:.66rem;color:var(--lux-ink-mute);
letter-spacing:.06em;margin-top:3px;
}
.picker-close{
width:28px;height:28px;border-radius:3px;background:transparent;
border:1px solid var(--lux-line);color:var(--lux-ink-mute);cursor:pointer;
display:flex;align-items:center;justify-content:center;
transition:.15s;
}
.picker-close:hover{background:var(--lux-bg-3);color:var(--lux-ink);}
.picker-close svg{width:12px;height:12px;}
.picker-toolbar{
display:flex;gap:10px;padding:12px 22px;
background:var(--lux-bg-0);
border-bottom:1px solid var(--lux-line);
align-items:center;
}
.picker-search{
flex:1;display:flex;align-items:center;gap:8px;
padding:8px 12px;background:var(--lux-bg-1);
border:1px solid var(--lux-line-bold);
border-radius:var(--lux-r-sm);
}
.picker-search svg{width:13px;height:13px;color:var(--lux-ink-mute);flex-shrink:0;}
.picker-search input{
flex:1;background:none;border:none;outline:none;
font-family:var(--font-mono);font-size:.78rem;letter-spacing:.04em;
color:var(--lux-ink);
}
.picker-search input::placeholder{
color:var(--lux-ink-mute);text-transform:uppercase;letter-spacing:.16em;font-size:.66rem;
}
.picker-search kbd{
font-family:var(--font-mono);font-size:.6rem;color:var(--lux-ink-mute);
border:1px solid var(--lux-line);border-radius:2px;
padding:1px 5px;background:var(--lux-bg-0);
}
.color-toggle{
display:flex;align-items:center;gap:8px;padding:8px 12px;
background:var(--lux-bg-1);border:1px solid var(--lux-line-bold);
border-radius:var(--lux-r-sm);cursor:pointer;
font-family:var(--font-mono);font-size:.62rem;font-weight:600;
letter-spacing:.18em;text-transform:uppercase;
color:var(--lux-ink-dim);
}
.color-toggle .swatch{
width:14px;height:14px;border-radius:2px;
background:var(--ch);
border:1px solid color-mix(in srgb, var(--ch) 60%, var(--lux-line-bold));
box-shadow:0 0 0 0 color-mix(in srgb, var(--ch) 40%, transparent);
}
.color-toggle:hover{color:var(--lux-ink);}
.picker-tabs{
display:flex;gap:0;padding:0 22px;
background:var(--lux-bg-0);border-bottom:1px solid var(--lux-line);
overflow-x:auto;
}
.picker-tab{
appearance:none;background:none;border:none;
padding:11px 16px;font-family:var(--font-mono);
font-size:.62rem;font-weight:600;
letter-spacing:.2em;text-transform:uppercase;
color:var(--lux-ink-mute);cursor:pointer;
border-bottom:2px solid transparent;
transition:.15s;display:flex;align-items:center;gap:8px;
white-space:nowrap;
}
.picker-tab .count{
font-size:.55rem;color:var(--lux-ink-faint);letter-spacing:.05em;
background:var(--lux-bg-2);padding:1px 5px;border-radius:99px;
}
.picker-tab:hover{color:var(--lux-ink-dim);}
.picker-tab.is-active{
color:var(--ch);
border-bottom-color:var(--ch);
}
.picker-tab.is-active .count{
background:color-mix(in srgb, var(--ch) 14%, transparent);
color:var(--ch);
}
.picker-recent{
display:flex;align-items:center;gap:14px;
padding:12px 22px;border-bottom:1px solid var(--lux-line);
background:var(--lux-bg-0);
}
.picker-recent .label{
font-family:var(--font-mono);font-size:.58rem;font-weight:600;
letter-spacing:.22em;text-transform:uppercase;color:var(--lux-ink-mute);
flex-shrink:0;
}
.picker-recent .strip{
display:flex;gap:6px;flex:1;overflow-x:auto;
}
.picker-recent .icon-tile{width:34px;height:34px;}
.picker-recent .icon-tile svg{width:18px;height:18px;}
.picker-grid-wrap{
padding:18px 22px 14px;max-height:360px;overflow-y:auto;
background:var(--lux-bg-1);
}
.picker-cat{
font-family:var(--font-mono);font-size:.58rem;font-weight:600;
letter-spacing:.24em;text-transform:uppercase;color:var(--lux-ink-mute);
margin:14px 0 10px;display:flex;align-items:center;gap:10px;
}
.picker-cat:first-child{margin-top:0;}
.picker-cat::after{content:'';flex:1;height:1px;background:var(--lux-line);}
.picker-grid{
display:grid;grid-template-columns:repeat(8,1fr);gap:6px;
}
.icon-tile{
--ch:var(--ch-cyan);
appearance:none;border:none;
width:42px;height:42px;
background:var(--lux-bg-0);
border:1px solid var(--lux-line);
border-radius:3px;color:var(--lux-ink-dim);
display:flex;align-items:center;justify-content:center;
cursor:pointer;transition:.15s;position:relative;
}
.icon-tile svg{
width:20px;height:20px;
stroke:currentColor;stroke-width:1.5;
fill:none;stroke-linecap:round;stroke-linejoin:round;
}
.icon-tile:hover{
color:var(--ch);
border-color:color-mix(in srgb, var(--ch) 50%, var(--lux-line));
background:color-mix(in srgb, var(--ch) 8%, var(--lux-bg-0));
box-shadow:0 0 12px color-mix(in srgb, var(--ch) 22%, transparent);
}
.icon-tile.is-selected{
color:var(--ch);
border-color:var(--ch);
background:color-mix(in srgb, var(--ch) 16%, var(--lux-bg-0));
box-shadow:0 0 16px color-mix(in srgb, var(--ch) 35%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--ch) 35%, transparent);
}
.icon-tile.is-selected::after{
content:'';position:absolute;top:3px;right:3px;
width:6px;height:6px;border-radius:50%;
background:var(--ch);box-shadow:0 0 6px var(--ch);
}
.icon-tile .name{display:none;}
.picker-foot{
display:flex;align-items:center;gap:10px;
padding:14px 22px;border-top:1px solid var(--lux-line);
background:var(--lux-bg-0);
}
.picker-foot .hint{
font-family:var(--font-mono);font-size:.6rem;letter-spacing:.16em;
text-transform:uppercase;color:var(--lux-ink-mute);
margin-right:auto;display:flex;align-items:center;gap:8px;
}
.picker-foot .hint kbd{
border:1px solid var(--lux-line);border-radius:2px;
padding:1px 5px;background:var(--lux-bg-1);
color:var(--lux-ink-dim);
}
.picker-btn{
appearance:none;border:1px solid var(--lux-line-bold);
padding:8px 16px;border-radius:var(--lux-r-sm);
background:var(--lux-bg-1);color:var(--lux-ink-dim);
font-family:var(--font-mono);font-size:.66rem;font-weight:600;
letter-spacing:.16em;text-transform:uppercase;cursor:pointer;
transition:.15s;
}
.picker-btn:hover{color:var(--lux-ink);background:var(--lux-bg-3);}
.picker-btn--ghost{border-color:transparent;background:transparent;}
.picker-btn--primary{
background:var(--ch);color:#fff;border-color:var(--ch);
box-shadow:0 0 18px color-mix(in srgb, var(--ch) 40%, transparent);
}
.picker-btn--primary:hover{filter:brightness(1.08);color:#fff;background:var(--ch);}
.picker-btn--danger{
color:var(--ch-coral);
border-color:color-mix(in srgb, var(--ch-coral) 40%, var(--lux-line-bold));
}
.picker-btn--danger:hover{
background:color-mix(in srgb, var(--ch-coral) 12%, transparent);
color:var(--ch-coral);
}
/* =====================================================================
Notes
===================================================================== */
.notes{
margin-top:64px;padding:30px 34px;
background:var(--lux-bg-1);border:1px solid var(--lux-line);
border-radius:var(--lux-r-md);position:relative;overflow:hidden;
}
.notes::before{
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
background:linear-gradient(180deg,var(--ch-signal),var(--ch-cyan),var(--ch-violet));
}
.notes h2{
font-family:var(--font-display);font-size:1.6rem;font-weight:800;
letter-spacing:-.01em;margin-bottom:18px;
}
.notes ul{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px 28px;}
.notes li{
font-size:.86rem;color:var(--lux-ink-dim);line-height:1.55;
padding-left:18px;position:relative;
}
.notes li::before{
content:'';position:absolute;left:0;top:.65em;width:8px;height:1px;background:var(--ch-signal);
}
.notes strong{color:var(--lux-ink);font-weight:700;}
.notes code{
font-family:var(--font-mono);font-size:.78rem;
background:var(--lux-bg-0);border:1px solid var(--lux-line);
padding:1px 5px;border-radius:3px;color:var(--ch-cyan);
}
@media (max-width: 880px){
.anatomy{grid-template-columns:1fr;}
.variants{grid-template-columns:repeat(2,1fr);}
.picker-grid{grid-template-columns:repeat(6,1fr);}
}
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<!-- ── Icon library (subset of a Lucide-like set) ── -->
<defs>
<symbol id="i-mouse" viewBox="0 0 24 24"><rect x="6" y="3" width="12" height="18" rx="6"/><line x1="12" y1="7" x2="12" y2="12"/></symbol>
<symbol id="i-keyboard" viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="13" rx="2"/><line x1="6" y1="10" x2="6.01" y2="10"/><line x1="10" y1="10" x2="10.01" y2="10"/><line x1="14" y1="10" x2="14.01" y2="10"/><line x1="18" y1="10" x2="18.01" y2="10"/><line x1="6" y1="14" x2="6.01" y2="14"/><line x1="18" y1="14" x2="18.01" y2="14"/><line x1="9" y1="14" x2="15" y2="14"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-tv" viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="14" rx="2"/><polyline points="17 2 12 7 7 2"/></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></symbol>
<symbol id="i-board" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><rect x="7" y="7" width="6" height="6"/><line x1="15" y1="7" x2="17" y2="7"/><line x1="15" y1="10" x2="17" y2="10"/><line x1="7" y1="16" x2="9" y2="16"/><line x1="7" y1="19" x2="9" y2="19"/><circle cx="17" cy="16" r="1.5"/><circle cx="13" cy="19" r="1"/></symbol>
<symbol id="i-gpu" viewBox="0 0 24 24"><rect x="2" y="7" width="20" height="10" rx="1"/><circle cx="8" cy="12" r="2.5"/><circle cx="14" cy="12" r="2.5"/><line x1="22" y1="9" x2="22" y2="15"/></symbol>
<symbol id="i-ram" viewBox="0 0 24 24"><rect x="2" y="8" width="20" height="9"/><rect x="5" y="11" width="2" height="3"/><rect x="9" y="11" width="2" height="3"/><rect x="13" y="11" width="2" height="3"/><rect x="17" y="11" width="2" height="3"/><line x1="2" y1="17" x2="22" y2="17"/><line x1="6" y1="17" x2="6" y2="20"/><line x1="18" y1="17" x2="18" y2="20"/></symbol>
<symbol id="i-ssd" viewBox="0 0 24 24"><rect x="3" y="6" width="18" height="12" rx="2"/><line x1="3" y1="14" x2="21" y2="14"/><circle cx="7" cy="16.5" r=".7"/><circle cx="10" cy="16.5" r=".7"/><circle cx="17" cy="9.5" r="1.3"/></symbol>
<symbol id="i-router" viewBox="0 0 24 24"><rect x="2" y="14" width="20" height="6" rx="2"/><line x1="6.5" y1="17" x2="6.51" y2="17"/><line x1="10" y1="17" x2="10.01" y2="17"/><path d="M5 10c0-3.87 3.13-7 7-7s7 3.13 7 7"/><path d="M8.5 10c0-1.93 1.57-3.5 3.5-3.5s3.5 1.57 3.5 3.5"/></symbol>
<symbol id="i-camera" viewBox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></symbol>
<symbol id="i-mic" viewBox="0 0 24 24"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></symbol>
<symbol id="i-speaker" viewBox="0 0 24 24"><rect x="4" y="2" width="16" height="20" rx="2"/><circle cx="12" cy="14" r="4"/><line x1="12" y1="6" x2="12.01" y2="6"/></symbol>
<symbol id="i-headphones" viewBox="0 0 24 24"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3z"/><path d="M3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></symbol>
<symbol id="i-controller" viewBox="0 0 24 24"><line x1="6" y1="11" x2="10" y2="11"/><line x1="8" y1="9" x2="8" y2="13"/><line x1="15" y1="12" x2="15.01" y2="12"/><line x1="18" y1="10" x2="18.01" y2="10"/><path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59L2 14a2 2 0 0 0 2 2h.5a3 3 0 0 0 2.7-1.7L8 12.5h8l.8 1.8a3 3 0 0 0 2.7 1.7H20a2 2 0 0 0 2-2l-.7-5.41A4 4 0 0 0 17.32 5z"/></symbol>
<symbol id="i-bulb" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></symbol>
<symbol id="i-strip" viewBox="0 0 24 24"><rect x="2" y="9" width="20" height="6" rx="1"/><circle cx="6" cy="12" r="1.2" fill="currentColor"/><circle cx="10" cy="12" r="1.2" fill="currentColor"/><circle cx="14" cy="12" r="1.2" fill="currentColor"/><circle cx="18" cy="12" r="1.2" fill="currentColor"/></symbol>
<symbol id="i-panel" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="12" cy="8" r="1.2" fill="currentColor"/><circle cx="16" cy="8" r="1.2" fill="currentColor"/><circle cx="8" cy="12" r="1.2" fill="currentColor"/><circle cx="12" cy="12" r="1.2" fill="currentColor"/><circle cx="16" cy="12" r="1.2" fill="currentColor"/><circle cx="8" cy="16" r="1.2" fill="currentColor"/><circle cx="12" cy="16" r="1.2" fill="currentColor"/><circle cx="16" cy="16" r="1.2" fill="currentColor"/></symbol>
<symbol id="i-ring" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/></symbol>
<symbol id="i-lamp" viewBox="0 0 24 24"><path d="M8 2h8l3 7H5z"/><path d="M12 9v9"/><path d="M9 18h6"/></symbol>
<symbol id="i-spot" viewBox="0 0 24 24"><path d="M6 4h12l-2 8H8z"/><line x1="12" y1="12" x2="12" y2="22"/><line x1="9" y1="22" x2="15" y2="22"/></symbol>
<symbol id="i-bed" viewBox="0 0 24 24"><path d="M2 18v-7a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v7"/><path d="M2 18h20"/><path d="M2 22v-4"/><path d="M22 22v-4"/><circle cx="7" cy="11" r="1.5"/></symbol>
<symbol id="i-sofa" viewBox="0 0 24 24"><path d="M20 9V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v2"/><path d="M2 13a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v4H2z"/><line x1="6" y1="17" x2="6" y2="20"/><line x1="18" y1="17" x2="18" y2="20"/></symbol>
<symbol id="i-desk" viewBox="0 0 24 24"><line x1="2" y1="9" x2="22" y2="9"/><line x1="2" y1="6" x2="22" y2="6"/><line x1="6" y1="9" x2="6" y2="20"/><line x1="18" y1="9" x2="18" y2="20"/><line x1="6" y1="15" x2="18" y2="15"/></symbol>
<symbol id="i-window" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="12" y1="3" x2="12" y2="21"/></symbol>
<symbol id="i-door" viewBox="0 0 24 24"><path d="M19 22V4a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v18"/><line x1="2" y1="22" x2="22" y2="22"/><circle cx="14" cy="13" r=".7" fill="currentColor"/></symbol>
<symbol id="i-fan" viewBox="0 0 24 24"><circle cx="12" cy="12" r="2"/><path d="M12 4a4 4 0 0 1 4 4c0 2-4 6-4 6s-4-4-4-6a4 4 0 0 1 4-4z"/><path d="M20 12a4 4 0 0 1-4 4c-2 0-6-4-6-4s4-4 6-4a4 4 0 0 1 4 4z"/><path d="M12 20a4 4 0 0 1-4-4c0-2 4-6 4-6s4 4 4 6a4 4 0 0 1-4 4z"/><path d="M4 12a4 4 0 0 1 4-4c2 0 6 4 6 4s-4 4-6 4a4 4 0 0 1-4-4z"/></symbol>
<symbol id="i-thermo" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="2"/><line x1="12" y1="6" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="18"/><line x1="6" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="18" y2="12"/></symbol>
<symbol id="i-power" viewBox="0 0 24 24"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></symbol>
<symbol id="i-wifi" viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0 1 14 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></symbol>
<symbol id="i-bt" viewBox="0 0 24 24"><polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"/></symbol>
<symbol id="i-antenna" viewBox="0 0 24 24"><line x1="2" y1="22" x2="22" y2="22"/><line x1="12" y1="22" x2="12" y2="2"/><path d="M5 8c0-3 3-5 7-5s7 2 7 5"/><path d="M8 11c0-2 2-3 4-3s4 1 4 3"/></symbol>
<symbol id="i-server" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="6" rx="1"/><rect x="2" y="13" width="20" height="6" rx="1"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="6" x2="10.01" y2="6"/><line x1="10" y1="16" x2="10.01" y2="16"/></symbol>
<symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/><line x1="4.93" y1="4.93" x2="6.34" y2="6.34"/><line x1="17.66" y1="17.66" x2="19.07" y2="19.07"/><line x1="4.93" y1="19.07" x2="6.34" y2="17.66"/><line x1="17.66" y1="6.34" x2="19.07" y2="4.93"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-flame" viewBox="0 0 24 24"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></symbol>
<symbol id="i-leaf" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19.2 2.96c1 4.34.06 9.65-3.4 13.04A6.96 6.96 0 0 1 11 20z"/><path d="M2 21c0-3 1.85-5.36 5.08-6"/></symbol>
<symbol id="i-music" viewBox="0 0 24 24"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></symbol>
<symbol id="i-film" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="2"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></symbol>
<symbol id="i-ha" viewBox="0 0 24 24"><path d="M3 12l9-9 9 9"/><path d="M5 10v10h14V10"/><circle cx="12" cy="14" r="2"/></symbol>
<symbol id="i-cloud" viewBox="0 0 24 24"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></symbol>
<symbol id="i-edit" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4z"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
<symbol id="i-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol>
<symbol id="i-stop" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="1"/></symbol>
<symbol id="i-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></symbol>
</defs>
</svg>
<div class="page">
<header class="page-head">
<div>
<div class="eyebrow">Proposal · Card · Custom Icon</div>
<h1>A face plate for<br>every <em>module.</em></h1>
<p class="lede">Cards inherit the channel-color stripe and instrument badges that already define the rack. Adding a user-chosen <code>icon</code> turns each card into something the user actually <em>recognises at a glance</em> — a mouse becomes a mouse, a motherboard becomes a motherboard. The badge stays as the type-of-thing label; the icon answers <strong>which thing</strong>. Optional, channel-tinted, and slotted in at the leading edge of <code>.mod-head</code> so the rest of the head row's typography is untouched.</p>
</div>
<div class="theme-toggle" role="group" aria-label="Theme">
<button class="is-active" data-theme-set="dark">DARK</button>
<button data-theme-set="light">LIGHT</button>
</div>
</header>
<!-- ===================================================================== -->
<div class="section-title"><span class="num">01</span> Before / After &nbsp;<span class="tag">CH · SIGNAL</span></div>
<div class="grid">
<!-- BEFORE — current device card (badge-only head) -->
<article class="module is-running" data-ch="signal">
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">WLED · OUT</span>
<div class="mod-name"><span>Living Room Strip</span></div>
<div class="mod-meta">192.168.1.42 · v0.14</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span><span class="led on blink"></span><span class="led on blink"></span>
</div>
</div>
<div class="mod-metrics">
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">144</div></div>
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">8<small>ms</small></div></div>
<div class="mod-metric"><div class="k">CHIP</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">WS2812B</div></div>
</div>
<div class="mod-fader">
<span class="mod-fader__k">Bright</span>
<div class="mod-fader__track"><div class="mod-fader__fill" style="width:78%"></div></div>
<span class="mod-fader__v">198</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>ONLINE</span></div>
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
<button class="mod-btn mod-btn-icon" title="Refresh"><svg width="14" height="14"><use href="#i-refresh"/></svg></button>
<button class="mod-btn mod-btn-icon" title="Settings"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
<div style="position:absolute;left:50%;top:6px;transform:translateX(-50%);font-family:var(--font-mono);font-size:.55rem;letter-spacing:.24em;text-transform:uppercase;color:var(--lux-ink-mute);background:var(--lux-bg-0);border:1px solid var(--lux-line);padding:2px 8px;border-radius:99px;">Before</div>
</article>
<!-- AFTER — same card, with icon plate -->
<article class="module is-running" data-ch="signal">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" type="button" aria-label="Change icon">
<svg><use href="#i-strip"/></svg>
<span class="mod-icon__edit" aria-hidden="true"><svg><use href="#i-edit"/></svg></span>
</button>
<div class="mod-id">
<span class="mod-badge">WLED · OUT</span>
<div class="mod-name"><span>Living Room Strip</span></div>
<div class="mod-meta">192.168.1.42 · v0.14</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span><span class="led on blink"></span><span class="led on blink"></span>
</div>
</div>
<div class="mod-metrics">
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">144</div></div>
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">8<small>ms</small></div></div>
<div class="mod-metric"><div class="k">CHIP</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">WS2812B</div></div>
</div>
<div class="mod-fader">
<span class="mod-fader__k">Bright</span>
<div class="mod-fader__track"><div class="mod-fader__fill" style="width:78%"></div></div>
<span class="mod-fader__v">198</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>ONLINE</span></div>
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
<button class="mod-btn mod-btn-icon" title="Refresh"><svg width="14" height="14"><use href="#i-refresh"/></svg></button>
<button class="mod-btn mod-btn-icon" title="Settings"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
<div style="position:absolute;left:50%;top:6px;transform:translateX(-50%);font-family:var(--font-mono);font-size:.55rem;letter-spacing:.24em;text-transform:uppercase;color:var(--ch-signal);background:var(--lux-bg-0);border:1px solid color-mix(in srgb,var(--ch-signal) 50%,var(--lux-line));padding:2px 8px;border-radius:99px;">After · Strip</div>
</article>
</div>
<!-- ===================================================================== -->
<div class="section-title"><span class="num">02</span> Anatomy &nbsp;<span class="tag" style="--ch-signal:var(--ch-cyan);color:var(--ch-cyan);border-color:color-mix(in srgb, var(--ch-cyan) 30%, transparent);background:color-mix(in srgb, var(--ch-cyan) 10%, transparent);">Plate</span></div>
<div class="anatomy">
<div class="anatomy-stage">
<div class="module" data-ch="cyan" style="background:transparent;border:none;padding:0;">
<div class="mod-icon" aria-hidden="true">
<svg><use href="#i-board"/></svg>
<span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span>
</div>
</div>
<span class="anno anno-1"><b>52 × 52</b> · plate</span>
<span class="anno anno-2">channel · <b>tint</b></span>
<span class="anno anno-3"><b>edit pin</b> · on hover</span>
<span class="anno anno-4">corner · <b>silkscreen</b></span>
</div>
<div class="anatomy-copy">
<h3>One plate.<br><em>Five quiet signals.</em></h3>
<p>The plate isn't a box around an icon. It's a small instrument-panel face that re-uses the card's channel color, the corner-bracket silkscreen, and the modular-rack scanline texture. Touch it and it lifts. Live cards make it breathe.</p>
<ul class="spec-list">
<li><span class="k">Size</span><div><strong>52 × 52 px</strong> on cards · <strong>40 × 40</strong> on dashboard tiles · <strong>34 × 34</strong> on perf charts. Scales with the card's existing breakpoints — no new layout math.</div></li>
<li><span class="k">Tint</span><div>Inherits <code>--ch</code> from the card (same variable that drives stripe, badge, fader). Pick a palette icon → it picks up the channel automatically.</div></li>
<li><span class="k">Override</span><div>Optional per-card hex via the picker's <strong>color toggle</strong>. Stored as <code>device.icon_color</code>; falls back to <code>--ch</code> when null.</div></li>
<li><span class="k">States</span><div><strong>Idle</strong> · <strong>Running</strong> (breathing) · <strong>Offline</strong> (desaturated) · <strong>Fault</strong> (coral) · <strong>Empty</strong> (dashed placeholder).</div></li>
<li><span class="k">Storage</span><div>Single field on the entity: <code>icon: "motherboard"</code>. Backwards-compatible — null hides the plate and the head reverts to today's badge-led layout.</div></li>
</ul>
</div>
</div>
<!-- ===================================================================== -->
<div class="section-title"><span class="num">03</span> States</div>
<div class="variants">
<div class="variant" data-ch="signal">
<div class="label"><b>IDLE</b> · default</div>
<div class="stage">
<div class="mod-icon">
<svg><use href="#i-mouse"/></svg>
</div>
</div>
<div class="desc">Resting state · channel-tinted · corner bracket visible · scanline texture.</div>
</div>
<div class="variant" data-ch="cyan">
<div class="label"><b>RUNNING</b> · live</div>
<div class="stage">
<div class="module is-running" style="background:transparent;border:none;padding:0;">
<div class="mod-icon">
<svg><use href="#i-camera"/></svg>
</div>
</div>
</div>
<div class="desc">2.6s breath syncs to the patch dot pulse · widens the stripe in lockstep.</div>
</div>
<div class="variant" data-ch="amber">
<div class="label"><b>OFFLINE</b></div>
<div class="stage">
<div class="module" data-state="offline" style="background:transparent;border:none;padding:0;">
<div class="mod-icon">
<svg><use href="#i-tv"/></svg>
</div>
</div>
</div>
<div class="desc">Glyph desaturates to <code>--lux-ink-mute</code> · plate keeps its outline so the slot stays anchored.</div>
</div>
<div class="variant" data-ch="coral">
<div class="label"><b>FAULT</b> / <b>EMPTY</b></div>
<div class="stage" style="display:flex;flex-direction:row;gap:10px;align-items:center;justify-content:center;">
<div class="module" data-state="fault" style="background:transparent;border:none;padding:0;">
<div class="mod-icon" style="--plate-size:62px;width:62px;height:62px;">
<svg style="width:30px;height:30px;"><use href="#i-bulb"/></svg>
</div>
</div>
<div class="mod-icon is-empty" style="--plate-size:62px;width:62px;height:62px;">
<svg style="width:18px;height:18px;"><use href="#i-plus"/></svg>
</div>
</div>
<div class="desc">Fault tints to coral. Empty state is dashed — a discoverable invitation, not a hole.</div>
</div>
</div>
<!-- ===================================================================== -->
<div class="section-title"><span class="num">04</span> Coverage &nbsp;<span class="tag" style="--ch-signal:var(--ch-magenta);color:var(--ch-magenta);border-color:color-mix(in srgb, var(--ch-magenta) 30%, transparent);background:color-mix(in srgb, var(--ch-magenta) 10%, transparent);">All Card Types</span></div>
<div class="grid">
<!-- WLED · LED Strip -->
<article class="module is-running" data-ch="signal">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-strip"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">WLED · OUT</span>
<div class="mod-name"><span>Couch Underglow</span></div>
<div class="mod-meta">192.168.1.81 · v0.15</div>
</div>
<div class="mod-leds"><span class="led on"></span><span class="led on blink"></span><span class="led on blink"></span></div>
</div>
<div class="mod-metrics">
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">90</div></div>
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">12<small>ms</small></div></div>
<div class="mod-metric"><div class="k">CHIP</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">SK6812</div></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>LIVE · OUT-2</span></div>
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Motherboard (mock-style desktop ambient) -->
<article class="module" data-ch="cyan">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-board"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">OPENRGB · OUT</span>
<div class="mod-name"><span>ROG Strix Z790-E</span></div>
<div class="mod-meta">openrgb://localhost:6742/3</div>
</div>
<div class="mod-leds"><span class="led on"></span><span class="led on blink"></span><span class="led"></span></div>
</div>
<div class="mod-metrics">
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">22</div></div>
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">3<small>ms</small></div></div>
<div class="mod-metric"><div class="k">ZONE</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">AURA</div></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>STANDBY</span></div>
<button class="mod-btn mod-btn-go"><span>START</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Mouse -->
<article class="module" data-ch="cyan">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-mouse"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">OPENRGB · OUT</span>
<div class="mod-name"><span>G502 Lightspeed</span></div>
<div class="mod-meta">openrgb://localhost:6742/4</div>
</div>
<div class="mod-leds"><span class="led on"></span></div>
</div>
<div class="mod-metrics mod-metrics--2">
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">3</div></div>
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">2<small>ms</small></div></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>READY</span></div>
<button class="mod-btn mod-btn-go"><span>START</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Keyboard -->
<article class="module is-running" data-ch="cyan">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-keyboard"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">OPENRGB · OUT</span>
<div class="mod-name"><span>Keychron Q1 Pro</span></div>
<div class="mod-meta">openrgb://localhost:6742/2</div>
</div>
<div class="mod-leds"><span class="led on"></span><span class="led on blink"></span><span class="led on"></span></div>
</div>
<div class="mod-metrics">
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">81</div></div>
<div class="mod-metric"><div class="k">LAT</div><div class="v signal">4<small>ms</small></div></div>
<div class="mod-metric"><div class="k">ZONE</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">PER-KEY</div></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>LIVE · OUT-3</span></div>
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Bedside lamp (HA Light target) -->
<article class="module" data-ch="signal">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-bulb"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">HA · LIGHT</span>
<div class="mod-name"><span>Hue Bedside Lamp</span></div>
<div class="mod-meta">light.bedside_lamp · 2700K</div>
</div>
<div class="mod-leds"><span class="led on"></span></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>STANDBY</span></div>
<button class="mod-btn mod-btn-go"><span>START</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Camera/Screen capture (source) -->
<article class="module is-running" data-ch="cyan">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-monitor"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">SCREEN · IN</span>
<div class="mod-name"><span>Primary Display</span></div>
<div class="mod-meta">3440×1440 · DXGI · HDR</div>
</div>
<div class="mod-leds"><span class="led on blink"></span><span class="led on"></span></div>
</div>
<div class="mod-metrics">
<div class="mod-metric"><div class="k">FPS</div><div class="v signal">59.7</div></div>
<div class="mod-metric"><div class="k">RES</div><div class="v" style="font-size:.92rem;font-family:var(--font-mono);font-weight:700;letter-spacing:.04em;">3440</div></div>
<div class="mod-metric"><div class="k">CPU</div><div class="v">3<small>%</small></div></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>CAPTURING</span></div>
<button class="mod-btn mod-btn-stop"><svg width="12" height="12"><use href="#i-stop"/></svg><span>STOP</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Microphone (audio source) -->
<article class="module is-running" data-ch="magenta">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-mic"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">AUDIO · IN</span>
<div class="mod-name"><span>Røde NT-USB Mini</span></div>
<div class="mod-meta">48kHz · stereo · 12.4dB</div>
</div>
<div class="mod-leds"><span class="led on blink"></span><span class="led on"></span></div>
</div>
<div class="mod-fader">
<span class="mod-fader__k">Level</span>
<div class="mod-fader__track"><div class="mod-fader__fill" style="width:62%"></div></div>
<span class="mod-fader__v">62</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>STREAMING</span></div>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- GPU (mock) -->
<article class="module" data-ch="amber">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-gpu"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">OPENRGB · OUT</span>
<div class="mod-name"><span>RTX 4080 Aorus Master</span></div>
<div class="mod-meta">openrgb://localhost:6742/1</div>
</div>
<div class="mod-leds"><span class="led on"></span><span class="led"></span></div>
</div>
<div class="mod-metrics mod-metrics--2">
<div class="mod-metric"><div class="k">PIXELS</div><div class="v">12</div></div>
<div class="mod-metric"><div class="k">TEMP</div><div class="v">52<small>°C</small></div></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>STANDBY</span></div>
<button class="mod-btn mod-btn-go"><span>START</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Game controller (game integration) -->
<article class="module" data-ch="amber">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-controller"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">GAME · IN</span>
<div class="mod-name"><span>Cyberpunk 2077</span></div>
<div class="mod-meta">SDK · health · ammo · radio</div>
</div>
<div class="mod-leds"><span class="led on"></span></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>WAITING</span></div>
<button class="mod-btn"><span>HOOK</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Empty/unset card -->
<article class="module" data-ch="violet">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon is-empty" aria-label="Add icon"><svg><use href="#i-plus"/></svg></button>
<div class="mod-id">
<span class="mod-badge">CLOCK · SYNC</span>
<div class="mod-name"><span>Studio Tempo</span></div>
<div class="mod-meta">120 BPM · 4/4 · master</div>
</div>
<div class="mod-leds"><span class="led on blink"></span></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>NO ICON</span></div>
<button class="mod-btn"><span>PICK ICON</span></button>
</div>
</article>
<!-- Fault state (offline router) -->
<article class="module" data-ch="coral" data-state="fault" style="opacity:.92;">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-router"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">WLED · OUT</span>
<div class="mod-name"><span>Garage Strip</span></div>
<div class="mod-meta">10.0.4.18 · timeout</div>
</div>
<div class="mod-leds"><span class="led fault"></span></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>OFFLINE · 2h 14m</span></div>
<button class="mod-btn"><span>RETRY</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
<!-- Speaker (audio source) -->
<article class="module" data-ch="magenta">
<div class="mod-head mod-head--with-icon">
<button class="mod-icon" aria-label="Change icon"><svg><use href="#i-speaker"/></svg><span class="mod-icon__edit"><svg><use href="#i-edit"/></svg></span></button>
<div class="mod-id">
<span class="mod-badge">AUDIO · IN</span>
<div class="mod-name"><span>Studio Monitor L+R</span></div>
<div class="mod-meta">loopback · 44.1kHz</div>
</div>
<div class="mod-leds"><span class="led on"></span></div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>STANDBY</span></div>
<button class="mod-btn mod-btn-go"><span>START</span></button>
<button class="mod-btn mod-btn-icon"><svg width="14" height="14"><use href="#i-settings"/></svg></button>
</div>
</article>
</div>
<!-- ===================================================================== -->
<div class="section-title"><span class="num">05</span> Picker &nbsp;<span class="tag" style="--ch-signal:var(--ch-violet);color:var(--ch-violet);border-color:color-mix(in srgb, var(--ch-violet) 30%, transparent);background:color-mix(in srgb, var(--ch-violet) 10%, transparent);">Modal · Open</span></div>
<div class="picker-stage">
<div class="picker">
<div class="picker-head">
<div class="preview">
<div class="mod-icon" data-preview style="--ch:var(--ch-cyan);">
<svg><use href="#i-board"/></svg>
</div>
</div>
<div class="meta">
<div class="eyebrow-mini">Card · ROG Strix Z790-E</div>
<h2>Choose an icon</h2>
<div class="sub">Tinted with the card's channel · click any tile to apply.</div>
</div>
<button class="picker-close" aria-label="Close"><svg><use href="#i-x"/></svg></button>
</div>
<div class="picker-toolbar">
<label class="picker-search">
<svg><use href="#i-search"/></svg>
<input type="text" placeholder="Search · Try &quot;mouse&quot;, &quot;ring&quot;, &quot;sun&quot;…" value="">
<kbd>⌘K</kbd>
</label>
<button class="color-toggle" type="button">
<span class="swatch" data-swatch></span>
<span>Match channel</span>
</button>
</div>
<div class="picker-tabs" role="tablist">
<button class="picker-tab is-active" role="tab" data-cat="all">All <span class="count">42</span></button>
<button class="picker-tab" role="tab" data-cat="hardware">Hardware <span class="count">10</span></button>
<button class="picker-tab" role="tab" data-cat="lighting">Lighting <span class="count">7</span></button>
<button class="picker-tab" role="tab" data-cat="rooms">Rooms <span class="count">7</span></button>
<button class="picker-tab" role="tab" data-cat="media">Media <span class="count">5</span></button>
<button class="picker-tab" role="tab" data-cat="signal">Signal <span class="count">5</span></button>
<button class="picker-tab" role="tab" data-cat="ambience">Ambience <span class="count">8</span></button>
</div>
<div class="picker-recent">
<span class="label">Recent</span>
<div class="strip">
<button class="icon-tile"><svg><use href="#i-mouse"/></svg></button>
<button class="icon-tile"><svg><use href="#i-keyboard"/></svg></button>
<button class="icon-tile"><svg><use href="#i-strip"/></svg></button>
<button class="icon-tile"><svg><use href="#i-bulb"/></svg></button>
<button class="icon-tile"><svg><use href="#i-monitor"/></svg></button>
<button class="icon-tile"><svg><use href="#i-mic"/></svg></button>
<button class="icon-tile"><svg><use href="#i-controller"/></svg></button>
</div>
</div>
<div class="picker-grid-wrap">
<div class="picker-cat">Hardware</div>
<div class="picker-grid">
<button class="icon-tile" title="Motherboard"><svg><use href="#i-board"/></svg></button>
<button class="icon-tile is-selected" title="CPU"><svg><use href="#i-cpu"/></svg></button>
<button class="icon-tile" title="GPU"><svg><use href="#i-gpu"/></svg></button>
<button class="icon-tile" title="RAM"><svg><use href="#i-ram"/></svg></button>
<button class="icon-tile" title="SSD"><svg><use href="#i-ssd"/></svg></button>
<button class="icon-tile" title="Mouse"><svg><use href="#i-mouse"/></svg></button>
<button class="icon-tile" title="Keyboard"><svg><use href="#i-keyboard"/></svg></button>
<button class="icon-tile" title="Controller"><svg><use href="#i-controller"/></svg></button>
<button class="icon-tile" title="Headphones"><svg><use href="#i-headphones"/></svg></button>
<button class="icon-tile" title="Server"><svg><use href="#i-server"/></svg></button>
</div>
<div class="picker-cat">Lighting</div>
<div class="picker-grid">
<button class="icon-tile" title="Bulb"><svg><use href="#i-bulb"/></svg></button>
<button class="icon-tile" title="LED Strip"><svg><use href="#i-strip"/></svg></button>
<button class="icon-tile" title="LED Panel"><svg><use href="#i-panel"/></svg></button>
<button class="icon-tile" title="Halo Ring"><svg><use href="#i-ring"/></svg></button>
<button class="icon-tile" title="Floor Lamp"><svg><use href="#i-lamp"/></svg></button>
<button class="icon-tile" title="Spotlight"><svg><use href="#i-spot"/></svg></button>
<button class="icon-tile" title="Power"><svg><use href="#i-power"/></svg></button>
</div>
<div class="picker-cat">Rooms &amp; Furniture</div>
<div class="picker-grid">
<button class="icon-tile" title="Bed"><svg><use href="#i-bed"/></svg></button>
<button class="icon-tile" title="Sofa"><svg><use href="#i-sofa"/></svg></button>
<button class="icon-tile" title="Desk"><svg><use href="#i-desk"/></svg></button>
<button class="icon-tile" title="Window"><svg><use href="#i-window"/></svg></button>
<button class="icon-tile" title="Door"><svg><use href="#i-door"/></svg></button>
<button class="icon-tile" title="Fan"><svg><use href="#i-fan"/></svg></button>
<button class="icon-tile" title="Thermostat"><svg><use href="#i-thermo"/></svg></button>
</div>
<div class="picker-cat">Media</div>
<div class="picker-grid">
<button class="icon-tile" title="Monitor"><svg><use href="#i-monitor"/></svg></button>
<button class="icon-tile" title="TV"><svg><use href="#i-tv"/></svg></button>
<button class="icon-tile" title="Camera"><svg><use href="#i-camera"/></svg></button>
<button class="icon-tile" title="Mic"><svg><use href="#i-mic"/></svg></button>
<button class="icon-tile" title="Speaker"><svg><use href="#i-speaker"/></svg></button>
</div>
<div class="picker-cat">Signal</div>
<div class="picker-grid">
<button class="icon-tile" title="WiFi"><svg><use href="#i-wifi"/></svg></button>
<button class="icon-tile" title="Bluetooth"><svg><use href="#i-bt"/></svg></button>
<button class="icon-tile" title="Antenna"><svg><use href="#i-antenna"/></svg></button>
<button class="icon-tile" title="Router"><svg><use href="#i-router"/></svg></button>
<button class="icon-tile" title="Cloud"><svg><use href="#i-cloud"/></svg></button>
</div>
<div class="picker-cat">Ambience</div>
<div class="picker-grid">
<button class="icon-tile" title="Sun"><svg><use href="#i-sun"/></svg></button>
<button class="icon-tile" title="Moon"><svg><use href="#i-moon"/></svg></button>
<button class="icon-tile" title="Flame"><svg><use href="#i-flame"/></svg></button>
<button class="icon-tile" title="Leaf"><svg><use href="#i-leaf"/></svg></button>
<button class="icon-tile" title="Music"><svg><use href="#i-music"/></svg></button>
<button class="icon-tile" title="Film"><svg><use href="#i-film"/></svg></button>
<button class="icon-tile" title="Home"><svg><use href="#i-ha"/></svg></button>
<button class="icon-tile" title="TV"><svg><use href="#i-tv"/></svg></button>
</div>
</div>
<div class="picker-foot">
<div class="hint"><kbd></kbd> Apply &nbsp;·&nbsp; <kbd>Esc</kbd> Cancel</div>
<button class="picker-btn picker-btn--danger">Remove icon</button>
<button class="picker-btn picker-btn--ghost">Cancel</button>
<button class="picker-btn picker-btn--primary">Apply</button>
</div>
</div>
</div>
<!-- ===================================================================== -->
<aside class="notes">
<h2>Implementation hints</h2>
<ul>
<li><strong>Schema:</strong> add <code>icon: str | None</code> and <code>icon_color: str | None</code> to the entity dataclass and Pydantic schema. Null on both → today's behaviour, no plate, no migration risk.</li>
<li><strong>Icon library:</strong> ship a curated subset of <code>lucide</code> as inline SVG <code>&lt;symbol&gt;</code>s in <code>icon-library.svg</code> — referenced by id (<code>&lt;use href="#i-mouse"/&gt;</code>). Tree-shaken at build time, no runtime import.</li>
<li><strong>Render slot:</strong> a new optional <code>head.icon: { id: string, color?: string }</code> on <code>ModHeadOpts</code>. <code>renderModHead</code> emits the plate before <code>.mod-id</code> when present and adds <code>mod-head--with-icon</code> for the head row alignment tweak.</li>
<li><strong>Picker entry points:</strong> ① click the plate on a card · ② new "Icon" row in <code>device-settings</code> modal · ③ <code>data-i18n</code> action in the kebab menu (&ldquo;Change icon&hellip;&rdquo;).</li>
<li><strong>Persistence:</strong> stored on the entity itself, not in a separate file — same JSON store, no migration needed for null values.</li>
<li><strong>Dashboard convergence:</strong> <code>.dashboard-target</code> already uses <code>.mod-head</code>, so the plate renders identically there. Smaller plate on <code>.perf-chart-card</code> via the <code>--plate-size</code> override.</li>
<li><strong>Search index:</strong> tag each icon with synonyms ("ssd → drive, disk, storage") so localisation lands cleanly through <code>t('icon.ssd.aliases')</code>.</li>
<li><strong>Keyboard:</strong> picker is <code>role="dialog"</code>; focus trap, <kbd></kbd> applies, <kbd>Esc</kbd> cancels, <kbd>⌘K</kbd> jumps to search. Arrow-key grid navigation matches the existing <code>IconSelect</code> component.</li>
</ul>
</aside>
</div>
<script>
(() => {
// Theme toggle
const root = document.documentElement;
document.querySelectorAll('[data-theme-set]').forEach(b => {
b.addEventListener('click', () => {
const t = b.dataset.themeSet;
root.dataset.theme = t;
document.querySelectorAll('[data-theme-set]').forEach(x => x.classList.toggle('is-active', x === b));
});
});
// Picker — clicking a tile updates preview + selection
const grid = document.querySelector('.picker-grid-wrap');
const preview = document.querySelector('[data-preview] svg use');
const swatch = document.querySelector('[data-swatch]');
const channels = ['var(--ch-cyan)', 'var(--ch-signal)', 'var(--ch-magenta)', 'var(--ch-amber)', 'var(--ch-violet)', 'var(--ch-coral)'];
let chIdx = 0;
grid.addEventListener('click', (e) => {
const tile = e.target.closest('.icon-tile');
if (!tile) return;
grid.querySelectorAll('.icon-tile.is-selected').forEach(x => x.classList.remove('is-selected'));
tile.classList.add('is-selected');
const useEl = tile.querySelector('svg use');
if (useEl && preview) preview.setAttribute('href', useEl.getAttribute('href'));
});
// Color toggle cycles channel for the preview
document.querySelector('.color-toggle')?.addEventListener('click', () => {
chIdx = (chIdx + 1) % channels.length;
const ch = channels[chIdx];
const previewPlate = document.querySelector('[data-preview]');
if (previewPlate) previewPlate.style.setProperty('--ch', ch);
if (swatch) swatch.style.setProperty('background', ch);
});
// Tabs (visual only)
document.querySelectorAll('.picker-tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('.picker-tab').forEach(x => x.classList.toggle('is-active', x === t));
});
});
})();
</script>
</body>
</html>