Files
ledgrab/docs/cards-redesign-demo-v2.html
alexei.dolgolyov a56569b02f feat(ui): cards redesign + settings, modal, toolbar polish
Dashboard cards (mod-card system)
- New mod-card / mod-menu modules backing dashboard cards
- Reworked card colors, sections, dashboard layout, perf charts
- Channel-stripe styling, hairline borders, signal-flow animation
  on running cards, refined metric grid

Multiselect bulk toolbar
- Replaced tri-state checkbox with explicit Select-all / Deselect-all
  icon buttons; both disable when not applicable
- Dim + slight blur on non-selected siblings during selection mode so
  the active picks pop; selected card gains a subtle lift + primary-color
  glow halo
- Bulk tick uses ICON_CHECK from the icon registry (was U+2713) and
  scale-pops in via a cubic-bezier overshoot keyframe
- Toolbar restyled with luxury gradient bg, top accent stripe, glass
  blur, neon hover glows on each button group

Settings modal
- Tab bar converted to icon-only (cog / hard-drive / bell / palette /
  refresh / help) so labels never overflow at any locale; title and
  aria-label preserve translated names. Tabs distribute evenly via
  flex: 1 1 0 + space-around — no overflow possible
- IconSelect auto-populates <option> elements when the underlying select
  is empty, fixing the blank notification triggers (root cause: setting
  .value on an empty select is a no-op)
- Tab activation calls scrollIntoView on the active button as a safety
  net for narrow viewports

Modal exit animation
- Added symmetric fadeOut + slideDown keyframes; .modal.closing applies
  them with animation-fill-mode: forwards
- Modal.forceClose() defers display:none until animationend (with timer
  fallback). State cleanup (focus, body lock, stack) runs immediately so
  callers querying state get correct values
- isOpen returns false during the close animation; open() cancels any
  in-flight close so re-open works during the animation
- prefers-reduced-motion disables all modal animations

Locale picker
- Dropped redundant English/Русский/中文 long-form labels — picker now
  shows only EN / RU / ZH
- IconSelect trigger/cell hides empty icon/label slots via :empty so the
  layout collapses cleanly for minimal items

Filter input (cards section)
- Embedded magnifier icon via data URI (no HTML change); monospace
  uppercase placeholder, lux-bg-0 background, neon focus ring with inset
  shadow + outer glow
- Reset button only shows when the input has content (CSS-only via
  :placeholder-shown sibling selector — JS-resilient)

Snack toast
- Glass background (gradient + backdrop-blur) with top channel-color
  accent stripe matching the modal/toolbar language
- Per-type --toast-ch drives border/glow/timer color (success → primary,
  error → danger, info → info)
- Undo button gets a tinted hover with channel-color halo

Top header toolbar
- Removed hairline border from .header-btn for a flatter look; hover
  keeps the subtle background tint and primary-color glow

Device URL hyperlink
- Styled .mod-meta__link to pick up the card's --ch accent (instead of
  inheriting browser-blue underline). Dotted underline at rest solidifies
  on hover; soft text-shadow glow; web icon dims at rest, brightens on
  hover

Misc
- ICON_CHECK and ICON_HARD_DRIVE added to the icon registry
- Existing card-redesign demos checked in under docs/
- Removed obsolete docs/plans/device-typed-configs.md
2026-04-26 03:10:16 +03:00

1421 lines
67 KiB
HTML
Raw Permalink 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 · Entity Card · v2 (dashboard convergence)</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>
/* =====================================================================
ENTITY CARD v2 — propagates the existing .mod-* vocabulary from
dashboard.css to the entity-card grid. Same classes, same tokens,
same instrument-readout aesthetic. Net change: a single card system
spanning Dashboard, Targets, Sources, Profiles tabs.
===================================================================== */
: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;
/* channel palette — verbatim from base.css */
--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);
--primary-contrast:#fff;
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;
--primary-text:#2e7d32;
--primary-contrast:#fff;
--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;}
/* =================== Demo chrome =================== */
.page{max-width:1320px;margin:0 auto;}
.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:120px;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 5px 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);
font-family:var(--font-display);
}
.lede{
color:var(--lux-ink-dim);max-width:50ch;font-size:.95rem;line-height:1.55;
margin-top:14px;
}
.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;padding:0;
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:48px 0 18px;
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;
}
.grid{
display:grid;
grid-template-columns:repeat(auto-fill,minmax(min(370px,100%),1fr));
gap:14px;
}
/* =====================================================================
.module — entity-card shell, mirrors `.dashboard-target:has(.mod-head)`
from dashboard.css. Same class names below: .mod-head/.mod-id/...
===================================================================== */
.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;
}
/* Always-on left stripe — verbatim from .dashboard-target::before */
.module::before{
content:'';
position:absolute;left:0;top:0;bottom:0;width:3px;
background:var(--ch);
box-shadow:0 0 8px color-mix(in srgb, var(--ch) 40%, transparent);
opacity:.6;
transition:opacity .2s ease, box-shadow .2s ease, width .2s ease;
}
/* Silkscreened corner bracket — only on idle, only when there's no
kebab menu already occupying the corner. The kebab visually
replaces it for any card that has interactive controls. */
.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;
}
/* Modules with a head-row kebab don't get the idle bracket — too
much corner clutter. Running cards still get the bottom signal-
flow because that uses the same ::after at a different position. */
.module:has(.mod-menu-wrap):not(.is-running)::after{display:none;}
.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);
}
/* Running state — stripe widens, corner bracket → bottom signal-flow */
.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.is-running::after{
top:auto;right:auto;left:4px;bottom:0;
width:calc(100% - 4px);height:2px;
border:none;opacity:.7;
background:linear-gradient(90deg,
transparent 0%,
color-mix(in srgb, var(--ch) 85%, transparent) 50%,
transparent 100%);
background-size:30% 100%;background-repeat:no-repeat;
animation:signalFlow 2.4s linear infinite;
}
@keyframes signalFlow{
0%{background-position:-30% 0;}
100%{background-position:130% 0;}
}
/* Channel assignments — match dashboard.css mappings */
.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 — verbatim from dashboard.css with one tweak: drop
space-between so a third flex child (the kebab menu) can sit
alongside .mod-leds without the layout fighting itself. ─────── */
.mod-head{
display:flex;
align-items:flex-start;
gap:10px;
}
.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 2px 4px;
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;
position:relative;
}
/* Channel color marker — leading dot inside the badge. IS the card-color
picker trigger. Identity, not an action — lives with the data it labels. */
.mod-badge__color{
width:10px;height:10px;border-radius:50%;flex-shrink:0;
background:var(--ch);
border:var(--lux-hairline) solid color-mix(in srgb, var(--ch) 60%, var(--lux-line-bold));
cursor:pointer;
transition:transform .15s var(--ease), box-shadow .15s;
box-shadow:0 0 0 0 color-mix(in srgb, var(--ch) 40%, transparent);
}
.mod-badge__color:hover{
transform:scale(1.25);
box-shadow:0 0 0 3px color-mix(in srgb, var(--ch) 25%, transparent);
}
/* Custom-color override (user picked a personal hue) — swatch keeps its
channel-tint ring but fills with the picked color (set inline). */
.mod-badge__color[data-custom]{
background:var(--user-color);
}
.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-name > span:first-child{
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
}
.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 — recessed bezel with multiple LEDs ─────────────── */
.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);
transition:background .2s, box-shadow .2s;
}
.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 — instrument-style metric grid ────────────────── */
.mod-metrics{
display:grid;
grid-template-columns:1.2fr 1fr 1fr;
gap:0;
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;position:relative;
}
.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);
display:inline-flex;align-items:center;gap:4px;
}
.mod-metric .k > svg{width:10px;height:10px;flex-shrink:0;opacity:.85;}
/* THE big numeric readout — display font, instrument-grade */
.mod-metric .v{
font-family:var(--font-display);
font-size:2rem;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.has-errors .v,
.mod-metric.has-errors .k,
.mod-metric.has-errors .k svg{color:var(--ch-coral);}
.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;
}
/* Two-cell metric variant (when a card only has 2 readouts) */
.mod-metrics--2{grid-template-columns:1fr 1fr;}
/* Single-cell variant (e.g. just one big readout) */
.mod-metrics--1{grid-template-columns:1fr;}
/* ── .mod-foot — patch indicator + action buttons ───────────────── */
.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 — verbatim from dashboard.css */
.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;min-width:0;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-go{
background:var(--ch);
color:var(--primary-contrast);
border-color:var(--ch);
box-shadow:0 0 14px color-mix(in srgb, var(--ch) 35%, transparent);
}
.mod-btn-go:hover{filter:brightness(1.1);color:var(--primary-contrast);}
.mod-btn-stop{
color:var(--ch-coral);
border-color:color-mix(in srgb, var(--ch-coral) 40%, transparent);
}
.mod-btn-stop:hover{
background:color-mix(in srgb, var(--ch-coral) 12%, transparent);
color:var(--ch-coral);
}
.mod-btn svg{width:12px;height:12px;}
.mod-btn-icon{
padding:7px 9px; /* square variant for tertiary actions */
}
.mod-btn-icon svg{width:14px;height:14px;}
/* ── Overflow menu — sits in the head row as a flex sibling of
.mod-leds, NOT absolutely positioned over the LED bezel.
This is the v2.1 fix: the kebab and the LED cluster live
side-by-side in the same band, never overlapping. ────────── */
.mod-menu-wrap{
position:relative;
flex-shrink:0;
align-self:flex-start;
}
.mod-menu-btn{
appearance:none;background:transparent;border:none;
width:26px;height:26px;border-radius:3px;
color:var(--lux-ink-mute);
display:inline-flex;align-items:center;justify-content:center;
cursor:pointer;transition:.15s;
opacity:.45; /* persistent but quiet */
}
.module:hover .mod-menu-btn,
.mod-menu-wrap.is-open .mod-menu-btn{
opacity:1;
}
.mod-menu-btn:hover,
.mod-menu-wrap.is-open .mod-menu-btn{
color:var(--lux-ink);
background:var(--lux-bg-3);
}
.mod-menu-btn:focus-visible{
outline:none;
box-shadow:0 0 0 2px color-mix(in srgb, var(--ch) 50%, transparent);
}
.mod-menu-btn svg{width:16px;height:16px;}
/* The menu itself — rendered as position:fixed so it floats above
the card's overflow:hidden boundary (needed so short cards like
gradients don't clip the dropdown). JS anchors it to the kebab
button at open time, and any scroll/resize closes it. */
.mod-menu{
position:fixed;z-index:100;min-width:172px;
background:var(--lux-bg-1);
border:var(--lux-hairline) solid var(--lux-line-bold);
border-radius:var(--lux-r-sm);
box-shadow:0 12px 32px rgba(0,0,0,.45),
0 0 0 1px rgba(255,255,255,.02);
padding:4px;
display:none;
flex-direction:column;
gap:0;
transform-origin:top right;
animation:menuIn .12s var(--ease);
}
.mod-menu-wrap.is-open .mod-menu{display:flex;}
@keyframes menuIn{
from{opacity:0;transform:scale(.96) translateY(-4px);}
to{opacity:1;transform:none;}
}
.mod-menu__item{
appearance:none;background:transparent;border:none;text-align:left;
display:flex;align-items:center;gap:10px;
padding:8px 12px;border-radius:2px;
font-family:var(--font-mono);
font-size:.66rem;font-weight:500;
letter-spacing:.12em;text-transform:uppercase;
color:var(--lux-ink-dim);
cursor:pointer;transition:.12s;
white-space:nowrap;
width:100%;
}
.mod-menu__item:hover{
background:var(--lux-bg-3);
color:var(--lux-ink);
}
.mod-menu__item svg{width:14px;height:14px;flex-shrink:0;color:var(--lux-ink-mute);}
.mod-menu__item:hover svg{color:var(--lux-ink);}
.mod-menu__item kbd{
margin-left:auto;
font-family:var(--font-mono);font-size:.6rem;
color:var(--lux-ink-faint);letter-spacing:.04em;
}
.mod-menu__sep{
height:1px;margin:4px 6px;background:var(--lux-line);
}
.mod-menu__item--danger{color:var(--ch-coral);}
.mod-menu__item--danger svg{color:var(--ch-coral);opacity:.85;}
.mod-menu__item--danger:hover{
background:color-mix(in srgb, var(--ch-coral) 12%, transparent);
color:var(--ch-coral);
}
.mod-menu__item--danger:hover svg{color:var(--ch-coral);opacity:1;}
/* Hidden card — entire module dimmed + dashed border, the menu's
"Show" item replaces "Hide" */
.module.is-hidden{
opacity:.55;
border-style:dashed;
}
/* ── Specialised slots ─────────────────────────────────────────── */
/* Property chips — used by template/source cards in lieu of metrics
when the data is qualitative (filter chains, tags, crosslinks).
Replaces both .card-meta and .stream-card-prop with one system. */
.mod-chips{display:flex;flex-wrap:wrap;align-items:center;gap:6px;}
.chip{
display:inline-flex;align-items:center;gap:6px;
font-family:var(--font-mono);font-size:.66rem;font-weight:500;
letter-spacing:.04em;color:var(--lux-ink-dim);
background:transparent;
border:var(--lux-hairline) solid var(--lux-line);
padding:3px 9px;border-radius:999px;
cursor:default;transition:.15s;
}
.chip svg{width:11px;height:11px;flex-shrink:0;color:var(--ch);}
.chip--link{cursor:pointer;}
.chip--link:hover{
color:var(--lux-ink);
border-color:color-mix(in srgb, var(--ch) 40%, transparent);
background:color-mix(in srgb, var(--ch) 12%, transparent);
}
.chip--tag{
color:var(--ch);
border-color:color-mix(in srgb, var(--ch) 22%, transparent);
background:color-mix(in srgb, var(--ch) 10%, transparent);
}
.chip--err{
color:var(--ch-coral);
border-color:color-mix(in srgb, var(--ch-coral) 30%, transparent);
background:color-mix(in srgb, var(--ch-coral) 10%, transparent);
}
.chain-arrow{color:var(--ch);opacity:.65;font-size:.72rem;}
/* Brightness fader (for LED targets/devices) — channel-tinted */
.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;
}
/* Preview surface — gradient strip / asset thumb / live LED preview */
.mod-preview{
height:28px;border-radius:var(--lux-r-sm);
box-shadow:inset 0 0 0 1px var(--lux-line);
overflow:hidden;position:relative;
}
.mod-preview--tall{height:64px;}
.mod-preview__tag{
position:absolute;left:6px;bottom:4px;
font-family:var(--font-mono);font-size:.55rem;letter-spacing:.18em;
color:#fff;text-shadow:0 1px 2px #000;
}
/* Body description / supporting copy (small, dim) */
.mod-desc{
font-size:.82rem;color:var(--lux-ink-dim);line-height:1.45;
}
/* Drag handle — persistent low-key */
.mod-drag{
position:absolute;left:6px;top:8px;
width:10px;height:18px;cursor:grab;z-index:3;
display:flex;flex-direction:column;justify-content:space-between;
opacity:.3;transition:opacity .15s;
}
.module:hover .mod-drag{opacity:.85;}
.mod-drag span{display:block;width:100%;height:1px;background:var(--lux-ink-mute);}
/* ===================== NOTES ===================== */
.notes{
margin-top:64px;padding:28px 32px;
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-magenta),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);
}
.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;
margin-left:8px;vertical-align:middle;
}
</style>
</head>
<body>
<div class="page">
<header class="page-head">
<div>
<div class="eyebrow">PROPOSAL · v2 · Convergence</div>
<h1>One card system,<br>the dashboard's <em>vocabulary</em>.</h1>
<p class="lede">v2 drops the parallel "patchbay rail" I sketched in v1 and adopts the existing <code>.mod-head</code> / <code>.mod-leds</code> / <code>.mod-metrics</code> / <code>.mod-patch</code> / <code>.mod-btn</code> classes from <code>dashboard.css</code> verbatim. Same instrument-style numerics, same recessed LED clusters, same patch indicator. Entity cards inherit dashboard polish; nothing new gets invented.</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> Output zone &nbsp;<span class="tag">CH · SIGNAL</span></div>
<div class="grid">
<!-- LED device, online + running -->
<article class="module is-running" data-ch="signal">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch" title="Channel color"></span>
<button class="mod-icon-btn" title="Hide"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg></button>
<button class="mod-icon-btn mod-icon-btn--danger" title="Delete"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">LED · CH-01</span>
<div class="mod-name"><span>Living Room Strip</span></div>
<div class="mod-meta">192.168.1.42 · WLED v0.14 · RGB</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"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h4l3-9 4 18 3-9h4"/></svg> FPS</div>
<div class="v signal">59.7</div>
</div>
<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">8<small>ms</small></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>PATCHED · OUT-1</span></div>
<button class="mod-btn mod-btn-stop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="12" height="12" rx="1"/></svg> <span>STOP</span></button>
<button class="mod-btn mod-btn-icon" title="Settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg></button>
</div>
</article>
<!-- LED target (offline) -->
<article class="module" data-ch="signal" style="opacity:.78;">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger" title="Delete"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">LED · CH-02</span>
<div class="mod-name"><span>Bedroom Halo</span></div>
<div class="mod-meta">10.0.4.18 · Adalight 921k</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led fault"></span>
<span class="led"></span>
<span class="led"></span>
</div>
</div>
<div class="mod-metrics">
<div class="mod-metric">
<div class="k">FPS</div>
<div class="v" style="color:var(--lux-ink-mute);">— —</div>
</div>
<div class="mod-metric">
<div class="k">PIXELS</div>
<div class="v" style="color:var(--lux-ink-dim);">60</div>
</div>
<div class="mod-metric has-errors">
<div class="k"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> ERR</div>
<div class="v">14</div>
</div>
</div>
<div class="mod-chips">
<span class="chip chip--err">Connection refused · 2h 14m ago</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>OFFLINE</span></div>
<button class="mod-btn">RETRY</button>
<button class="mod-btn mod-btn-icon" title="Settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m11-7h-6m-6 0H1"/></svg></button>
</div>
</article>
<!-- HA Light Target -->
<article class="module" data-ch="signal">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger" title="Delete"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
<div class="mod-head">
<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 · color_temp 2700K</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span>
</div>
</div>
<div class="mod-chips">
<span class="chip chip--link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82M12 1v6m0 6v6"/></svg> Source · Cinematic</span>
<span class="chip chip--link">Brightness · Outdoor temp</span>
</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"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg> <span>START</span></button>
</div>
</article>
</div>
<div class="section-title"><span class="num">02</span> Input zone &nbsp;<span class="tag" style="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);">CH · CYAN</span> <span class="tag" style="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);">CH · MAGENTA</span></div>
<div class="grid">
<!-- Picture source -->
<article class="module" data-ch="cyan">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">SCREEN · IN</span>
<div class="mod-name"><span>Cinematic Capture</span></div>
<div class="mod-meta">Display 2 · MSS · BGRA</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led"></span>
<span class="led"></span>
</div>
</div>
<div class="mod-metrics mod-metrics--2">
<div class="mod-metric">
<div class="k">REGION</div>
<div class="v">3840×<wbr>1080</div>
</div>
<div class="mod-metric">
<div class="k">TARGET</div>
<div class="v">60<small>fps</small></div>
</div>
</div>
<div class="mod-chips">
<span class="chip chip--link">Pre-process · Cinematic CSPT</span>
<span class="chip">Letterbox crop</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>STANDBY</span></div>
<button class="mod-btn">TEST</button>
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
</div>
</article>
<!-- Audio FFT (running) -->
<article class="module is-running" data-ch="magenta">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">FFT · IN</span>
<div class="mod-name"><span>Spotify Loopback</span></div>
<div class="mod-meta">WASAPI · 48 kHz · stereo</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on blink"></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">PEAK</div>
<div class="v signal">-6.2<small>dB</small></div>
</div>
<div class="mod-metric">
<div class="k">BANDS</div>
<div class="v">32</div>
</div>
<div class="mod-metric">
<div class="k">CPU</div>
<div class="v">3.1<small>%</small></div>
</div>
</div>
<div class="mod-chips">
<span class="chip chip--tag">bass</span>
<span class="chip chip--tag">mids</span>
<span class="chip chip--tag">highs</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-stop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12" rx="1"/></svg> <span>STOP</span></button>
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
</div>
</article>
<!-- Value source HA -->
<article class="module" data-ch="cyan">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">VALUE · HA</span>
<div class="mod-name"><span>Outdoor temp</span></div>
<div class="mod-meta">sensor.outdoor_temp · linear · clamped</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span>
</div>
</div>
<div class="mod-metrics">
<div class="mod-metric">
<div class="k">NOW</div>
<div class="v signal">14.7<small>°</small></div>
</div>
<div class="mod-metric">
<div class="k">RANGE</div>
<div class="v">8 — 22</div>
</div>
<div class="mod-metric">
<div class="k">TICK</div>
<div class="v">2<small>s</small></div>
</div>
</div>
<div class="mod-chips">
<span class="chip">Bound to · brightness</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>POLLING</span></div>
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
</div>
</article>
</div>
<div class="section-title"><span class="num">03</span> Logic zone &nbsp;<span class="tag" style="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);">CH · VIOLET</span> <span class="tag" style="color:var(--ch-amber);border-color:color-mix(in srgb, var(--ch-amber) 30%, transparent);background:color-mix(in srgb, var(--ch-amber) 10%, transparent);">CH · AMBER</span></div>
<div class="grid">
<!-- Automation -->
<article class="module" data-ch="violet">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">AUTO · 07</span>
<div class="mod-name"><span>Movie Night</span></div>
<div class="mod-meta">Plex playing · 21:00 — 23:30</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span>
<span class="led"></span>
</div>
</div>
<div class="mod-chips">
<span class="chip chip--link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> 21:00 — 23:30</span>
<span class="chain-arrow">+</span>
<span class="chip chip--link">Plex playing</span>
<span class="chain-arrow"></span>
<span class="chip chip--link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> Cinema scene</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>ARMED</span></div>
<button class="mod-btn mod-btn-stop">DISABLE</button>
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
</div>
</article>
<!-- Scene preset -->
<article class="module" data-ch="violet">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">SCN · 04</span>
<div class="mod-name"><span>Sunset Warmth</span></div>
<div class="mod-meta">4 targets · captured 21 Apr · used by 2 automations</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led"></span>
<span class="led"></span>
</div>
</div>
<div class="mod-desc">Warm tungsten cast on every fixture for a 19:00 unwind. Recapture re-snapshots all targets in their current colors.</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>PRESET</span></div>
<button class="mod-btn">RECAPTURE</button>
<button class="mod-btn mod-btn-go"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> <span>ACTIVATE</span></button>
</div>
</article>
<!-- Sync clock (running) -->
<article class="module is-running" data-ch="violet">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">CLK · MASTER</span>
<div class="mod-name"><span>Master Tempo</span></div>
<div class="mod-meta">Square · 1/16 sub · drift ±0.3ms</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on blink"></span>
</div>
</div>
<div class="mod-metrics">
<div class="mod-metric">
<div class="k">BPM</div>
<div class="v signal">110.0</div>
</div>
<div class="mod-metric">
<div class="k">PHASE</div>
<div class="v">0.42</div>
</div>
<div class="mod-metric">
<div class="k">SUB</div>
<div class="v">1<small>/16</small></div>
</div>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>TICKING</span></div>
<button class="mod-btn">TAP</button>
<button class="mod-btn mod-btn-stop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="6" width="12" height="12"/></svg> <span>STOP</span></button>
</div>
</article>
<!-- Game integration -->
<article class="module" data-ch="amber">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">GAME · CSGO</span>
<div class="mod-name"><span>Counter-Strike 2</span></div>
<div class="mod-meta">GSI · port 3456 · 12 mapped events</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span>
<span class="led on"></span>
</div>
</div>
<div class="mod-chips">
<span class="chip chip--tag">flash</span>
<span class="chip chip--tag">defuse</span>
<span class="chip chip--tag">round-end</span>
<span class="chip">+9 more</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>LISTENING · 12s ago</span></div>
<button class="mod-btn">TEST</button>
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
</div>
</article>
</div>
<div class="section-title"><span class="num">04</span> Templates &amp; assets</div>
<div class="grid">
<!-- Capture template -->
<article class="module" data-ch="cyan">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">TPL · CAPTURE</span>
<div class="mod-name"><span>Desktop · 60 fps · region</span></div>
<div class="mod-meta">MSS · 3 keys configured · used by 5 sources</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led"></span>
</div>
</div>
<div class="mod-chips">
<span class="chip">crop</span><span class="chain-arrow"></span>
<span class="chip">downsample</span><span class="chain-arrow"></span>
<span class="chip">gamma</span><span class="chain-arrow"></span>
<span class="chip">color-correct</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>TEMPLATE</span></div>
<button class="mod-btn">TEST</button>
<button class="mod-btn">CLONE</button>
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
</div>
</article>
<!-- Color strip (running, with live preview) -->
<article class="module is-running" data-ch="signal">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">STRIP · MAPPED</span>
<div class="mod-name"><span>Cinema map · 144 px</span></div>
<div class="mod-meta">Letterbox · smooth 0.35</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span>
<span class="led on blink"></span>
</div>
</div>
<!-- Live LED preview is the headline element; replaces metrics grid -->
<div class="mod-preview" style="background:linear-gradient(90deg,
#200035 0%, #5a1ca8 8%, #c14b8e 18%, #ff7a59 30%,
#ffd56b 40%, #95e7c9 50%, #00d8ff 65%, #2d6cdf 78%,
#1c2554 90%, #050816 100%);"></div>
<div class="mod-chips">
<span class="chip chip--link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/></svg> Display 2</span>
<span class="chip chip--link">CSPT · 4 filters</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot is-live"></span><span>MAPPING</span></div>
<button class="mod-btn mod-btn-icon" title="Test"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg></button>
<button class="mod-btn mod-btn-icon" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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-4 9.5-9.5z"/></svg></button>
</div>
</article>
<!-- Gradient — preview replaces metrics -->
<article class="module" data-ch="signal">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">PALETTE · G-08</span>
<div class="mod-name"><span>Aurora <span style="font-size:.55rem;font-family:var(--font-mono);letter-spacing:.18em;color:var(--ch);background:color-mix(in srgb, var(--ch) 12%, transparent);border:1px solid color-mix(in srgb, var(--ch) 30%, transparent);padding:1px 5px;border-radius:3px;margin-left:6px;">BUILTIN</span></span></div>
<div class="mod-meta">5 stops · HSL space · loop</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span>
</div>
</div>
<div class="mod-preview" style="height:36px;background:linear-gradient(90deg,#003e6b 0%,#0085c4 25%,#22d3ee 55%,#a855f7 80%,#ec4899 100%);">
<span class="mod-preview__tag">5 STOPS · LOOP</span>
</div>
<div class="mod-chips">
<span class="chip">Used in 3 strips</span>
</div>
<div class="mod-foot">
<div class="mod-patch"><span class="patch-dot"></span><span>PRESET</span></div>
<button class="mod-btn">CLONE</button>
</div>
</article>
<!-- Asset (image) -->
<article class="module" data-ch="cyan">
<div class="mod-drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<div class="mod-corner-actions">
<span class="swatch"></span>
<button class="mod-icon-btn mod-icon-btn--danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/></svg></button>
</div>
<div class="mod-head">
<div class="mod-id">
<span class="mod-badge">ASSET · IMG</span>
<div class="mod-name"><span>cosmic-loop-001.png</span></div>
<div class="mod-meta">PNG · 1920×1080 · 412 KB</div>
</div>
<div class="mod-leds" aria-hidden="true">
<span class="led on"></span>
</div>
</div>
<div class="mod-preview mod-preview--tall" style="background:
radial-gradient(circle at 30% 40%,#a855f7 0,transparent 35%),
radial-gradient(circle at 70% 60%,#22d3ee 0,transparent 30%),
#07090e;"></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-icon" title="Download"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></button>
<button class="mod-btn">REPLACE</button>
</div>
</article>
</div>
<section class="notes">
<h2>How v2 maps to the dashboard</h2>
<ul>
<li><strong>Same shell, same classes.</strong> <code>.module</code> mirrors <code>.dashboard-target:has(.mod-head)</code> down to padding and the always-on left stripe at 0.6→1.0 opacity.</li>
<li><strong>Inline <code>.mod-badge</code> replaces my v1 top rail.</strong> Channel-tinted mono caps pill at the top-left of <code>.mod-id</code> — exactly the dashboard pattern, no extra band needed.</li>
<li><strong><code>.mod-leds</code> recessed cluster</strong> next to the title — multiple dots in a hairline bezel, real-hardware feel. Single LED card-types use one dot in the same bezel.</li>
<li><strong>Numerics use <code>var(--font-display)</code>.</strong> Big Shoulders Display, 2rem, 800-weight, tabular-nums. Already bundled in <code>fonts/big-shoulders-display-latin.woff2</code> and only used on dashboard + perf cards today.</li>
<li><strong><code>.mod-patch</code> footer label</strong> on every card: <code>PATCHED · OUT-1</code>, <code>STREAMING</code>, <code>OFFLINE</code>, <code>STANDBY</code>, <code>ARMED</code>, <code>PRESET</code>, <code>READY</code>. Channel-colored dot on the left, mono caps state label — the strongest "this is rack hardware" cue.</li>
<li><strong><code>.mod-btn</code> with text labels</strong> for primary/secondary actions in the footer (<code>START</code> / <code>STOP</code> / <code>EDIT</code>). Tertiary square <code>.mod-btn-icon</code> variant for settings.</li>
<li><strong>Card-color is identity, not an action.</strong> A 10px <code>.mod-badge__color</code> dot leads every <code>.mod-badge</code> — channel-tinted by default, fills with the user's picked color when overridden. Click to open the picker. Lives <em>with</em> the type label it tints, not in a destructive cluster.</li>
<li><strong>Single overflow menu</strong> (kebab) replaces the v2.0 corner huddle. Always visible at 0.34 opacity, brightens on card-hover. Click to open <code>Duplicate</code> / <code>Hide card</code> / ─── / <code>Delete</code>. One control, isolated, touch-and-keyboard accessible — and easily extended (<em>Move to section</em>, <em>Export</em>) without re-cramping the corner.</li>
<li><strong>Why this works:</strong> destructive actions are no longer adjacent to cosmetic ones (no more "delete next to color picker"); the menu groups management actions semantically; the corner reads as quiet metadata instead of a row of buttons; every interaction is discoverable without hover.</li>
<li><strong>Properties unify.</strong> <code>.card-meta</code> + <code>.stream-card-prop</code> → single <code>.chip</code> system with three modifiers: default, <code>.chip--link</code> for crosslinks, <code>.chip--tag</code> for tag pills, <code>.chip--err</code> for error states. Filter chains use chips + <code>.chain-arrow</code> separators.</li>
<li><strong>Three body slots</strong> compose every card type:
<code>.mod-metrics</code> (numeric readouts) ·
<code>.mod-chips</code> (qualitative metadata, crosslinks, filter chains) ·
<code>.mod-preview</code> (gradient strip / asset thumb / live LED preview).
Plus optional <code>.mod-fader</code> (LED targets) and <code>.mod-desc</code> (descriptions).</li>
</ul>
<h2 style="margin-top:42px;">v1 → v2 specifics</h2>
<ul>
<li>Dropped the 32px top rail band → moved to inline <code>.mod-badge</code> at top-left.</li>
<li>Mono LCD-style readouts → display-font instrument readouts at 2rem 800.</li>
<li>Single status dot → <code>.mod-leds</code> 13 LED bezel.</li>
<li>Square icon-only footer buttons → <code>.mod-btn</code> with text labels for state-changing actions; icon variant kept for settings/edit only.</li>
<li>Added <code>.mod-patch</code> footer indicator on every card.</li>
<li>Restored the corner bracket (idle) and the running signal-flow animation.</li>
<li><strong>Corner cluster killed.</strong> The "swatch + hide + delete" huddle is gone. Color is now a 10px identity dot inside the badge; hide and delete moved into a single overflow menu.</li>
<li>Field cleanup: paper-grain overlay removed (dashboard cards don't use it; not part of the established language).</li>
</ul>
<h2 style="margin-top:42px;">Implementation footprint</h2>
<ul>
<li><strong>CSS:</strong> the <code>.mod-*</code> rules already exist in <code>dashboard.css</code>. The entity-card variant just needs <code>.card:has(.mod-head)</code> / <code>.template-card:has(.mod-head)</code> selectors that copy <code>.dashboard-target:has(.mod-head)</code>'s padding/layout — or alias all three to a shared <code>.module</code> class.</li>
<li><strong>JS:</strong> <code>wrapCard()</code> in <code>card-colors.ts</code> already handles the wrapper, color stripe, and corner actions. Add an optional second signature: <code>wrapCard({ mod: { badge, name, meta, leds, metrics, chips, preview, fader, patch, primaryAction } })</code> that emits the dashboard markup. Existing <code>content/actions</code> path stays for incremental migration.</li>
<li><strong>Per feature:</strong> each <code>create*Card()</code> swaps its hand-rolled <code>.card-header</code> + <code>.card-subtitle</code> + <code>.stream-card-props</code> for the structured <code>mod</code> options. Localised, low-risk, can ship one card type per PR.</li>
<li><strong>Cards.css cleanup:</strong> remove the <code>[data-has-color="1"], .card-running</code> gate on the <code>::before</code> stripe so the channel signal becomes ambient. Single line change.</li>
</ul>
</section>
</div>
<script>
// ── Theme toggle ─────────────────────────────────────────────────
document.querySelectorAll('.theme-toggle button').forEach(b=>{
b.addEventListener('click',()=>{
document.documentElement.dataset.theme=b.dataset.themeSet;
document.querySelectorAll('.theme-toggle button').forEach(x=>x.classList.toggle('is-active',x===b));
});
});
// ── Card-color identity dot — leading element of every .mod-badge.
// Single click target, lives where the channel color reads as
// identity, not as an action sitting in a destructive cluster. ──
document.querySelectorAll('.mod-badge').forEach(b=>{
if(b.querySelector('.mod-badge__color')) return;
const dot=document.createElement('span');
dot.className='mod-badge__color';
dot.setAttribute('role','button');
dot.setAttribute('tabindex','0');
dot.setAttribute('aria-label','Change card color');
dot.title='Card color';
b.prepend(dot);
});
// Demo: a couple of cards have a user-picked custom color so the
// reader can see the override state.
const CUSTOM_OVERRIDES={
'Bedroom Halo':'#ffb800',
'Sunset Warmth':'#ff7a59',
};
document.querySelectorAll('.module').forEach(card=>{
const name=card.querySelector('.mod-name span')?.textContent?.trim();
const hex=name && CUSTOM_OVERRIDES[name];
if(hex){
const dot=card.querySelector('.mod-badge__color');
dot.dataset.custom='1';
dot.style.setProperty('--user-color',hex);
}
});
// Stub picker — not the focus of the demo
document.addEventListener('click',e=>{
const dot=e.target.closest('.mod-badge__color');
if(!dot) return;
const card=dot.closest('.module');
const name=card?.querySelector('.mod-name span')?.textContent?.trim()||'card';
alert(`(Demo) opens the inline color picker for "${name}". In production this calls registerColorPicker() from color-picker.ts.`);
});
// ── Replace every .mod-corner-actions with the overflow menu ──
const ICON_HIDE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
const ICON_CLONE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
const ICON_TRASH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>';
const ICON_KEBAB = '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/></svg>';
document.querySelectorAll('.mod-corner-actions').forEach(el=>{
// Detect what the card already had so we keep the menu honest:
// - card had a delete (.mod-icon-btn--danger) → keep Delete item
// (gradient builtin card has no delete → no Delete item)
const hadDelete = !!el.querySelector('.mod-icon-btn--danger');
const isBuiltin = !hadDelete;
const items=[];
items.push(`<button class="mod-menu__item" role="menuitem">${ICON_CLONE} Duplicate</button>`);
items.push(`<button class="mod-menu__item" role="menuitem">${ICON_HIDE} Hide card</button>`);
if(!isBuiltin){
items.push(`<div class="mod-menu__sep"></div>`);
items.push(`<button class="mod-menu__item mod-menu__item--danger" role="menuitem">${ICON_TRASH} Delete</button>`);
}
const wrap=document.createElement('div');
wrap.className='mod-menu-wrap';
wrap.innerHTML=`
<button class="mod-menu-btn" type="button" aria-haspopup="true" aria-expanded="false" aria-label="Card options" title="More actions">
${ICON_KEBAB}
</button>
<div class="mod-menu" role="menu">
${items.join('')}
</div>`;
// v2.1 fix: append the menu inside .mod-head as a flex sibling
// of .mod-leds — never absolutely positioned over the LED bezel.
const card = el.closest('.module');
const head = card?.querySelector('.mod-head');
el.remove();
if(head) head.appendChild(wrap);
else card?.appendChild(wrap); // fallback (cards without a head)
});
// ── Menu open/close behavior ─────────────────────────────────────
function _closeAllMenus(){
document.querySelectorAll('.mod-menu-wrap.is-open').forEach(w=>{
w.classList.remove('is-open');
w.querySelector('.mod-menu-btn')?.setAttribute('aria-expanded','false');
});
}
function _positionMenu(wrap){
// Anchor the menu to the kebab button's bottom-right corner,
// using fixed positioning so the card's overflow:hidden never
// clips short-card dropdowns (gradient, asset, etc.).
const btn=wrap.querySelector('.mod-menu-btn');
const menu=wrap.querySelector('.mod-menu');
if(!btn||!menu) return;
const r=btn.getBoundingClientRect();
const menuH=menu.offsetHeight||140;
const spaceBelow=window.innerHeight-r.bottom;
// Open upward when there's not enough room below
if(spaceBelow<menuH+12){
menu.style.top=`${r.top-menuH-4}px`;
menu.style.transformOrigin='bottom right';
}else{
menu.style.top=`${r.bottom+4}px`;
menu.style.transformOrigin='top right';
}
menu.style.right=`${window.innerWidth-r.right}px`;
menu.style.left='auto';
}
document.addEventListener('click',e=>{
const btn=e.target.closest('.mod-menu-btn');
if(btn){
e.stopPropagation();
const wrap=btn.closest('.mod-menu-wrap');
const open=wrap.classList.contains('is-open');
_closeAllMenus();
if(!open){
wrap.classList.add('is-open');
btn.setAttribute('aria-expanded','true');
// Position after the menu becomes visible so offsetHeight is accurate
requestAnimationFrame(()=>_positionMenu(wrap));
}
return;
}
if(!e.target.closest('.mod-menu')) _closeAllMenus();
});
document.addEventListener('keydown',e=>{
if(e.key==='Escape') _closeAllMenus();
});
window.addEventListener('scroll',_closeAllMenus,{passive:true,capture:true});
window.addEventListener('resize',_closeAllMenus,{passive:true});
// Demo: clicking a menu item shows what action would fire and
// closes the menu — production wires these to the existing
// hideCard / deleteX / cloneX functions.
document.addEventListener('click',e=>{
const item=e.target.closest('.mod-menu__item');
if(!item) return;
const card=item.closest('.module');
const name=card?.querySelector('.mod-name span')?.textContent?.trim()||'card';
const action=item.textContent.trim();
_closeAllMenus();
if(action==='Hide card'){
card.classList.toggle('is-hidden');
item.firstChild.replaceWith(card.classList.contains('is-hidden')?'Show card':'Hide card');
}else{
// Toast-style notification, demo only
const t=document.createElement('div');
t.textContent=`(Demo) "${action}" → "${name}"`;
Object.assign(t.style,{position:'fixed',bottom:'24px',left:'50%',transform:'translateX(-50%)',
background:'var(--lux-bg-1)',color:'var(--lux-ink)',
border:'1px solid var(--lux-line-bold)',padding:'10px 16px',borderRadius:'4px',
fontFamily:'var(--font-mono)',fontSize:'.75rem',letterSpacing:'.08em',
boxShadow:'0 8px 24px rgba(0,0,0,.4)',zIndex:9999});
document.body.appendChild(t);
setTimeout(()=>t.remove(),1800);
}
});
</script>
</body>
</html>