a56569b02f
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
1118 lines
58 KiB
HTML
1118 lines
58 KiB
HTML
<!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 · Patchbay redesign</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@600;800&family=JetBrains+Mono:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||
<style>
|
||
/* =====================================================================
|
||
PATCHBAY — entity card redesign for LedGrab
|
||
Aesthetic: studio-rack hardware module. Always-on channel signal,
|
||
tactile silkscreen labels, LCD-style readouts, tiny micro-motion.
|
||
Drops into the existing Lumenworks token system.
|
||
===================================================================== */
|
||
|
||
: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:8px;
|
||
|
||
/* channel palette — same hex values as production */
|
||
--ch-signal:#4CAF50; /* led / target */
|
||
--ch-cyan:#00d8ff; /* source / data */
|
||
--ch-magenta:#ff4ade; /* audio / fft */
|
||
--ch-amber:#ffb800; /* autostart / pending */
|
||
--ch-coral:#ff5e5e; /* offline / alarm */
|
||
--ch-violet:#8b7eff; /* automation / scenes */
|
||
|
||
--duration:.22s;
|
||
--ease:cubic-bezier(.16,1,.3,1);
|
||
}
|
||
|
||
[data-theme="dark"]{
|
||
--bg-page:#000;
|
||
--bg-card:#0e1014;
|
||
--bg-recess:#06080b;
|
||
--bg-chip:#0a0c10;
|
||
--line:#1f242c;
|
||
--line-bold:#2c333d;
|
||
--line-soft:#181c22;
|
||
--ink:#e6ebf2;
|
||
--ink-dim:#8b95a5;
|
||
--ink-mute:#5b6473;
|
||
--ink-faint:#3a414c;
|
||
--shadow:0 1px 0 rgba(255,255,255,.03), 0 14px 36px rgba(0,0,0,.55), 0 2px 8px rgba(0,0,0,.4);
|
||
--shadow-flat:0 1px 0 rgba(255,255,255,.025), 0 4px 12px rgba(0,0,0,.35);
|
||
--grain:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence baseFrequency='.9' numOctaves='2' seed='4'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .045 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
||
}
|
||
[data-theme="light"]{
|
||
--bg-page:#f0f1f4;
|
||
--bg-card:#fff;
|
||
--bg-recess:#f6f8fb;
|
||
--bg-chip:#f6f8fb;
|
||
--line:#dce1e8;
|
||
--line-bold:#bcc4d0;
|
||
--line-soft:#e8ebf0;
|
||
--ink:#0f1419;
|
||
--ink-dim:#4c5866;
|
||
--ink-mute:#6b7684;
|
||
--ink-faint:#a5afbc;
|
||
--shadow:0 1px 0 rgba(255,255,255,.6), 0 12px 28px rgba(15,20,25,.10), 0 2px 6px rgba(15,20,25,.06);
|
||
--shadow-flat:0 1px 0 rgba(255,255,255,.6), 0 3px 10px rgba(15,20,25,.06);
|
||
--grain:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence baseFrequency='.9' numOctaves='2' seed='4'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .035 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
||
--primary-color:#2e7d32;
|
||
--primary-text:#2e7d32;
|
||
--ch-signal:#2e7d32;
|
||
--ch-cyan:#006b88;
|
||
--ch-magenta:#b01a99;
|
||
--ch-amber:#a56a00;
|
||
--ch-coral:#d8392e;
|
||
--ch-violet:#5b4fd0;
|
||
}
|
||
|
||
*{box-sizing:border-box;margin:0;padding:0;}
|
||
html,body{background:var(--bg-page);color:var(--ink);font-family:var(--font-body);min-height:100vh;}
|
||
body{padding:48px 32px 80px;-webkit-font-smoothing:antialiased;}
|
||
|
||
/* =================== Page chrome (demo only) =================== */
|
||
.page{max-width:1280px;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(--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(--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,4rem);font-weight:800;
|
||
letter-spacing:-.02em;line-height:.92;
|
||
color:var(--ink);
|
||
}
|
||
h1 em{
|
||
font-style:normal;color:var(--ch-signal);
|
||
font-family:var(--font-display);
|
||
}
|
||
.lede{
|
||
color:var(--ink-dim);max-width:46ch;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(--line-bold);border-radius:999px;padding:0;
|
||
background:var(--bg-card);overflow:hidden;
|
||
}
|
||
.theme-toggle button{
|
||
appearance:none;background:none;border:none;color:var(--ink-mute);
|
||
padding:8px 16px;cursor:pointer;font:inherit;letter-spacing:inherit;
|
||
transition:color .2s,background .2s;
|
||
}
|
||
.theme-toggle button.is-active{
|
||
background:var(--ink);color:var(--bg-card);
|
||
}
|
||
|
||
/* =================== Section heading =================== */
|
||
.section-title{
|
||
font-family:var(--font-mono);font-size:.72rem;letter-spacing:.3em;text-transform:uppercase;
|
||
color:var(--ink-mute);margin:48px 0 16px;
|
||
display:flex;align-items:center;gap:14px;
|
||
}
|
||
.section-title::after{content:'';flex:1;height:1px;background:var(--line);}
|
||
.section-title .num{
|
||
font-family:var(--font-display);font-size:1.6rem;font-weight:800;
|
||
color:var(--ink);letter-spacing:0;
|
||
border-right:1px solid var(--line);padding-right:14px;
|
||
}
|
||
|
||
/* =================== Card grid =================== */
|
||
.grid{
|
||
display:grid;
|
||
grid-template-columns:repeat(auto-fill,minmax(340px,1fr));
|
||
gap:22px;
|
||
}
|
||
|
||
/* =====================================================================
|
||
CARD — patchbay module
|
||
===================================================================== */
|
||
.card{
|
||
--ch:var(--ch-signal);
|
||
--ch-mix-12:color-mix(in srgb, var(--ch) 12%, transparent);
|
||
--ch-mix-22:color-mix(in srgb, var(--ch) 22%, transparent);
|
||
--ch-mix-40:color-mix(in srgb, var(--ch) 40%, transparent);
|
||
--ch-mix-65:color-mix(in srgb, var(--ch) 65%, transparent);
|
||
|
||
position:relative;
|
||
background:var(--bg-card);
|
||
border:1px solid var(--line);
|
||
border-radius:var(--lux-r-md);
|
||
padding:0;
|
||
display:flex;flex-direction:column;
|
||
overflow:hidden;
|
||
transition:transform var(--duration) var(--ease),
|
||
box-shadow var(--duration) var(--ease),
|
||
border-color var(--duration) var(--ease);
|
||
isolation:isolate;
|
||
}
|
||
|
||
/* paper-grain overlay — subtle, only one decorative layer */
|
||
.card::before{
|
||
content:'';position:absolute;inset:0;pointer-events:none;
|
||
background-image:var(--grain);
|
||
background-size:180px 180px;
|
||
mix-blend-mode:overlay;opacity:.6;
|
||
z-index:0;
|
||
}
|
||
|
||
/* hover: lift, brighten border, intensify channel rail */
|
||
.card:hover{
|
||
transform:translateY(-3px);
|
||
border-color:color-mix(in srgb, var(--ch) 26%, var(--line-bold));
|
||
box-shadow:var(--shadow);
|
||
}
|
||
.card:hover .rail{opacity:1;}
|
||
.card:hover .rail__bar{transform:scaleX(1);}
|
||
|
||
/* ───────── CHANNEL RAIL — top edge, always on at low intensity ───────── */
|
||
.rail{
|
||
position:relative;
|
||
height:32px;
|
||
padding:0 14px 0 16px;
|
||
display:flex;align-items:center;justify-content:space-between;
|
||
background:linear-gradient(180deg,
|
||
color-mix(in srgb, var(--ch) 8%, transparent) 0%,
|
||
transparent 100%);
|
||
border-bottom:1px solid var(--line-soft);
|
||
opacity:.85;
|
||
transition:opacity var(--duration) var(--ease);
|
||
z-index:1;
|
||
}
|
||
/* the actual channel signal line — sits along the top edge */
|
||
.rail__bar{
|
||
position:absolute;left:0;top:0;height:2px;width:100%;
|
||
background:linear-gradient(90deg,
|
||
var(--ch) 0%,
|
||
var(--ch-mix-40) 60%,
|
||
transparent 100%);
|
||
transform:scaleX(.65);transform-origin:left;
|
||
transition:transform var(--duration) var(--ease), opacity var(--duration) var(--ease);
|
||
box-shadow:0 0 12px var(--ch-mix-40);
|
||
}
|
||
/* type label — silkscreen panel text */
|
||
.rail__label{
|
||
font-family:var(--font-display);
|
||
font-size:.78rem;font-weight:800;letter-spacing:.22em;text-transform:uppercase;
|
||
color:var(--ch);
|
||
display:inline-flex;align-items:center;gap:8px;
|
||
text-shadow:0 0 12px var(--ch-mix-22);
|
||
}
|
||
.rail__label .id{
|
||
font-family:var(--font-mono);font-size:.62rem;font-weight:500;
|
||
color:var(--ink-faint);letter-spacing:.18em;
|
||
}
|
||
/* tiny notch on the rail's right side — patchpoint */
|
||
.rail__notch{
|
||
width:14px;height:14px;border-radius:50%;
|
||
background:var(--bg-recess);
|
||
border:1px solid var(--line-bold);
|
||
position:relative;
|
||
box-shadow:inset 0 1px 1px rgba(0,0,0,.4);
|
||
}
|
||
.rail__notch::after{
|
||
content:'';position:absolute;inset:3px;border-radius:50%;
|
||
background:var(--ch);opacity:.32;
|
||
transition:opacity var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
|
||
}
|
||
|
||
/* ───────── BODY ───────── */
|
||
.body{
|
||
position:relative;z-index:1;
|
||
padding:16px 18px 14px;
|
||
display:flex;flex-direction:column;gap:12px;
|
||
flex:1;
|
||
}
|
||
|
||
.title-row{display:flex;align-items:center;gap:10px;min-width:0;}
|
||
.title{
|
||
font-family:var(--font-body);
|
||
font-size:1.1rem;font-weight:700;letter-spacing:-.01em;
|
||
color:var(--ink);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
flex:1;min-width:0;
|
||
}
|
||
/* status LED */
|
||
.led{
|
||
width:9px;height:9px;border-radius:50%;
|
||
flex-shrink:0;position:relative;
|
||
background:var(--ink-faint);
|
||
box-shadow:inset 0 0 0 1px rgba(0,0,0,.3);
|
||
}
|
||
.led--on{
|
||
background:var(--ch-signal);
|
||
box-shadow:0 0 0 0 var(--ch-mix-40),
|
||
0 0 10px var(--ch-mix-65),
|
||
inset 0 0 2px rgba(255,255,255,.4);
|
||
animation:ledPulse 2.4s ease-in-out infinite;
|
||
}
|
||
@keyframes ledPulse{
|
||
50%{box-shadow:0 0 0 4px transparent,0 0 14px var(--ch-mix-65),inset 0 0 2px rgba(255,255,255,.4);}
|
||
}
|
||
.led--off{background:var(--ch-coral);box-shadow:0 0 8px color-mix(in srgb, var(--ch-coral) 50%, transparent);}
|
||
.led--unknown{background:var(--ch-amber);box-shadow:0 0 8px color-mix(in srgb, var(--ch-amber) 35%, transparent);}
|
||
|
||
/* address pill — short identifier next to title */
|
||
.address{
|
||
font-family:var(--font-mono);
|
||
font-size:.62rem;font-weight:500;letter-spacing:.08em;
|
||
color:var(--ink-dim);
|
||
background:var(--bg-recess);
|
||
border:1px solid var(--line-soft);
|
||
padding:3px 8px;border-radius:3px;
|
||
flex-shrink:1;min-width:0;
|
||
text-overflow:ellipsis;overflow:hidden;
|
||
box-shadow:inset 0 1px 0 rgba(0,0,0,.2);
|
||
}
|
||
[data-theme="light"] .address{box-shadow:inset 0 1px 0 rgba(15,20,25,.04);}
|
||
|
||
/* ───────── READOUT — LCD-style metric strip ───────── */
|
||
.readout{
|
||
display:flex;align-items:stretch;gap:0;
|
||
background:var(--bg-recess);
|
||
border:1px solid var(--line-soft);
|
||
border-radius:var(--lux-r-sm);
|
||
padding:0;overflow:hidden;
|
||
box-shadow:inset 0 1px 2px rgba(0,0,0,.4);
|
||
}
|
||
[data-theme="light"] .readout{box-shadow:inset 0 1px 2px rgba(15,20,25,.06);}
|
||
.readout__cell{
|
||
flex:1;padding:7px 10px;
|
||
border-right:1px solid var(--line-soft);
|
||
display:flex;flex-direction:column;gap:2px;min-width:0;
|
||
}
|
||
.readout__cell:last-child{border-right:none;}
|
||
.readout__k{
|
||
font-family:var(--font-mono);font-size:.58rem;letter-spacing:.18em;text-transform:uppercase;
|
||
color:var(--ink-mute);
|
||
}
|
||
.readout__v{
|
||
font-family:var(--font-mono);font-size:.92rem;font-weight:700;
|
||
color:var(--ink);font-variant-numeric:tabular-nums;
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.readout__v--accent{color:var(--ch);text-shadow:0 0 12px var(--ch-mix-40);}
|
||
.readout__v--dim{color:var(--ink-mute);font-weight:500;}
|
||
|
||
/* ───────── PROPERTY CHIPS — secondary metadata ───────── */
|
||
.props{display:flex;flex-wrap:wrap;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(--ink-dim);
|
||
background:transparent;
|
||
border:1px solid var(--line);
|
||
padding:3px 9px;border-radius:999px;
|
||
transition:color .15s, border-color .15s, background .15s;
|
||
cursor:default;
|
||
}
|
||
.chip svg{width:11px;height:11px;flex-shrink:0;color:var(--ch);}
|
||
.chip--link{cursor:pointer;}
|
||
.chip--link:hover{
|
||
color:var(--ink);
|
||
border-color:var(--ch-mix-40);
|
||
background:var(--ch-mix-12);
|
||
}
|
||
.chip--tag{
|
||
color:var(--ch);
|
||
border-color:var(--ch-mix-22);
|
||
background:var(--ch-mix-12);
|
||
}
|
||
|
||
/* ───────── BRIGHTNESS / SLIDER (LED devices) ───────── */
|
||
.fader{
|
||
display:flex;align-items:center;gap:12px;
|
||
padding:8px 10px;
|
||
background:var(--bg-recess);
|
||
border:1px solid var(--line-soft);
|
||
border-radius:var(--lux-r-sm);
|
||
}
|
||
.fader__label{
|
||
font-family:var(--font-mono);font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;
|
||
color:var(--ink-mute);min-width:42px;
|
||
}
|
||
.fader__track{
|
||
position:relative;flex:1;height:6px;border-radius:99px;
|
||
background:linear-gradient(90deg,
|
||
color-mix(in srgb, var(--ch) 0%, transparent),
|
||
color-mix(in srgb, var(--ch) 30%, transparent));
|
||
overflow:hidden;
|
||
box-shadow:inset 0 1px 2px rgba(0,0,0,.4);
|
||
}
|
||
[data-theme="light"] .fader__track{box-shadow:inset 0 1px 2px rgba(15,20,25,.08);}
|
||
.fader__fill{
|
||
position:absolute;left:0;top:0;bottom:0;
|
||
background:linear-gradient(90deg,var(--ch-mix-40),var(--ch));
|
||
box-shadow:0 0 8px var(--ch-mix-65);
|
||
}
|
||
.fader__val{
|
||
font-family:var(--font-mono);font-size:.8rem;font-weight:700;
|
||
color:var(--ink);min-width:38px;text-align:right;
|
||
font-variant-numeric:tabular-nums;
|
||
}
|
||
|
||
/* ───────── FOOTER (action row) ───────── */
|
||
.footer{
|
||
position:relative;z-index:1;
|
||
margin-top:auto;
|
||
padding:10px 14px 12px;
|
||
display:flex;align-items:center;gap:6px;
|
||
background:linear-gradient(180deg,transparent,var(--bg-recess));
|
||
border-top:1px solid var(--line-soft);
|
||
}
|
||
/* fastener dots — tiny silkscreened rivets at the corners of the seam */
|
||
.footer::before,.footer::after{
|
||
content:'';position:absolute;top:-3px;width:5px;height:5px;border-radius:50%;
|
||
background:var(--bg-page);
|
||
box-shadow:inset 0 0 0 1px var(--line-bold), 0 0 0 1px var(--bg-card);
|
||
}
|
||
.footer::before{left:8px;}
|
||
.footer::after{right:8px;}
|
||
|
||
.btn{
|
||
appearance:none;background:transparent;border:1px solid var(--line);
|
||
color:var(--ink-dim);
|
||
width:30px;height:30px;border-radius:var(--lux-r-sm);
|
||
display:inline-flex;align-items:center;justify-content:center;
|
||
cursor:pointer;transition:.15s;
|
||
flex-shrink:0;
|
||
}
|
||
.btn:hover{color:var(--ink);border-color:var(--ch-mix-40);background:var(--ch-mix-12);}
|
||
.btn svg{width:14px;height:14px;}
|
||
.btn--danger:hover{color:var(--ch-coral);border-color:color-mix(in srgb, var(--ch-coral) 40%, transparent);background:color-mix(in srgb, var(--ch-coral) 10%, transparent);}
|
||
.btn--primary{
|
||
color:var(--bg-card);background:var(--ch);border-color:var(--ch);
|
||
margin-left:auto;
|
||
}
|
||
.btn--primary:hover{filter:brightness(1.1);}
|
||
|
||
.swatch{
|
||
width:18px;height:18px;border-radius:50%;
|
||
background:conic-gradient(red,orange,yellow,green,cyan,blue,magenta,red);
|
||
border:1px solid var(--line-bold);cursor:pointer;
|
||
margin-left:auto;
|
||
box-shadow:inset 0 0 0 2px var(--bg-card);
|
||
transition:transform .2s var(--ease);
|
||
}
|
||
.swatch:hover{transform:scale(1.15) rotate(45deg);}
|
||
|
||
/* =================== RUNNING / ACTIVE state =================== */
|
||
.card--running{
|
||
border-color:var(--ch-mix-40);
|
||
box-shadow:
|
||
0 0 0 1px var(--ch-mix-22),
|
||
var(--shadow-flat);
|
||
}
|
||
.card--running .rail__bar{transform:scaleX(1);}
|
||
.card--running .rail{opacity:1;}
|
||
.card--running .rail__notch::after{
|
||
opacity:1;
|
||
box-shadow:0 0 8px var(--ch),0 0 14px var(--ch-mix-65);
|
||
}
|
||
/* signal-flow at the bottom edge */
|
||
.card--running::after{
|
||
content:'';position:absolute;left:0;right:0;bottom:0;height:2px;
|
||
background:linear-gradient(90deg,
|
||
transparent 0%,
|
||
var(--ch) 50%,
|
||
transparent 100%);
|
||
background-size:35% 100%;background-repeat:no-repeat;
|
||
animation:signalFlow 2.4s linear infinite;
|
||
z-index:2;
|
||
}
|
||
@keyframes signalFlow{
|
||
0%{background-position:-35% 0;}
|
||
100%{background-position:135% 0;}
|
||
}
|
||
|
||
/* =================== OFFLINE state =================== */
|
||
.card--offline{opacity:.78;}
|
||
.card--offline .rail__bar{
|
||
background:linear-gradient(90deg,
|
||
var(--ch-coral) 0%,
|
||
color-mix(in srgb, var(--ch-coral) 30%, transparent) 60%,
|
||
transparent 100%);
|
||
box-shadow:none;
|
||
transform:scaleX(.4);
|
||
animation:offlineFlicker 4.5s ease-in-out infinite;
|
||
}
|
||
@keyframes offlineFlicker{
|
||
0%,40%,42%,100%{transform:scaleX(.4);}
|
||
41%{transform:scaleX(.55);}
|
||
}
|
||
|
||
/* =================== DRAG handle (always visible, low key) =================== */
|
||
.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:.35;transition:opacity .15s;
|
||
}
|
||
.card:hover .drag{opacity:.85;}
|
||
.drag span{display:block;width:100%;height:1px;background:var(--ink-mute);}
|
||
|
||
/* =================== CHANNEL ASSIGNMENTS =================== */
|
||
.card[data-ch="signal"] {--ch:var(--ch-signal);}
|
||
.card[data-ch="cyan"] {--ch:var(--ch-cyan);}
|
||
.card[data-ch="magenta"] {--ch:var(--ch-magenta);}
|
||
.card[data-ch="amber"] {--ch:var(--ch-amber);}
|
||
.card[data-ch="coral"] {--ch:var(--ch-coral);}
|
||
.card[data-ch="violet"] {--ch:var(--ch-violet);}
|
||
|
||
/* =================== Notes block =================== */
|
||
.notes{
|
||
margin-top:64px;padding:28px 32px;
|
||
background:var(--bg-card);border:1px solid var(--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:18px 28px;}
|
||
.notes li{
|
||
font-size:.88rem;color:var(--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(--ink);font-weight:700;}
|
||
.notes code{
|
||
font-family:var(--font-mono);font-size:.78rem;
|
||
background:var(--bg-recess);border:1px solid var(--line-soft);
|
||
padding:1px 5px;border-radius:3px;color:var(--ch-cyan);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
|
||
<header class="page-head">
|
||
<div>
|
||
<div class="eyebrow">PROPOSAL · v1 · Patchbay</div>
|
||
<h1>Entity card,<br>treated as <em>hardware</em>.</h1>
|
||
<p class="lede">Every entity is a module in a virtual rack. The channel system you already built — dormant on most cards today — becomes the primary identity signal: type, status, and "live" state read at a glance.</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 targets · LED devices</div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- LED device, online + running -->
|
||
<article class="card card--running" data-ch="signal">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">LED · OUT <span class="id">// CH-01</span></div>
|
||
<div class="rail__notch" title="Patch point"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--on" title="Online"></span>
|
||
<span class="title">Living Room Strip</span>
|
||
<span class="address">192.168.1.42</span>
|
||
</div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Pixels</div>
|
||
<div class="readout__v">144</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">FPS</div>
|
||
<div class="readout__v readout__v--accent">59.7</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Lat</div>
|
||
<div class="readout__v">8<span class="readout__v--dim">ms</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip"><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="10"/><circle cx="12" cy="12" r="3"/></svg> WLED · v0.14</span>
|
||
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg> RGB</span>
|
||
<span class="chip chip--link" title="Jump to source"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg> Display 2</span>
|
||
</div>
|
||
<div class="fader">
|
||
<span class="fader__label">Bright</span>
|
||
<div class="fader__track"><div class="fader__fill" style="width:78%"></div></div>
|
||
<span class="fader__val">198</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Refresh"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></svg></button>
|
||
<button class="btn" title="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></button>
|
||
<button class="btn" 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>
|
||
<span class="swatch" title="Channel color"></span>
|
||
<button class="btn 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"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- LED device, offline -->
|
||
<article class="card card--offline" data-ch="signal">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">LED · OUT <span class="id">// CH-02</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--off" title="Offline"></span>
|
||
<span class="title">Bedroom Halo</span>
|
||
<span class="address">10.0.4.18</span>
|
||
</div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Pixels</div>
|
||
<div class="readout__v readout__v--dim">60</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">FPS</div>
|
||
<div class="readout__v readout__v--dim">— —</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Last</div>
|
||
<div class="readout__v readout__v--dim">2h 14m</div>
|
||
</div>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip" style="--ch:var(--ch-coral);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);"><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="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> Connection refused</span>
|
||
<span class="chip">Adalight · 921k</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Retry"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/></svg></button>
|
||
<button class="btn" 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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
<!-- Audio source, running -->
|
||
<article class="card card--running" data-ch="magenta">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">FFT · IN <span class="id">// 48 kHz</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--on" title="Streaming"></span>
|
||
<span class="title">Spotify Loopback</span>
|
||
</div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Bands</div>
|
||
<div class="readout__v">32</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Peak</div>
|
||
<div class="readout__v readout__v--accent">-6.2 dB</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">CPU</div>
|
||
<div class="readout__v">3.1<span class="readout__v--dim">%</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip">WASAPI · stereo</span>
|
||
<span class="chip chip--tag">Bass · Mids · Highs</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="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></button>
|
||
<button class="btn" title="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></button>
|
||
<button class="btn" 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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
</div>
|
||
|
||
<div class="section-title"><span class="num">02</span> Sources & integrations</div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- Screen capture source -->
|
||
<article class="card" data-ch="cyan">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">SCREEN · IN <span class="id">// DISP-2</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--unknown" title="Idle"></span>
|
||
<span class="title">Cinematic Capture</span>
|
||
</div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Region</div>
|
||
<div class="readout__v">3840×1080</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Tgt</div>
|
||
<div class="readout__v">60fps</div>
|
||
</div>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip">Mss · BGRA</span>
|
||
<span class="chip chip--link">Pre-process · CSPT</span>
|
||
<span class="chip">Letterbox crop</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Test"><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></button>
|
||
<button class="btn" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
<!-- Automation -->
|
||
<article class="card" data-ch="violet">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">SCENE · LOGIC <span class="id">// AUTO-07</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--on" title="Armed"></span>
|
||
<span class="title">Movie Night</span>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip"><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="10"/><polyline points="12 6 12 12 16 14"/></svg> 21:00 — 23:30</span>
|
||
<span class="chip chip--link">When · Plex playing</span>
|
||
<span class="chip chip--link">→ Living + Bedroom</span>
|
||
<span class="chip chip--tag">cinema</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Run now"><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></button>
|
||
<button class="btn" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
<!-- Scene preset -->
|
||
<article class="card" data-ch="violet">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">SCENE · PRESET <span class="id">// SCN-04</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--unknown" title="Idle"></span>
|
||
<span class="title">Sunset Warmth</span>
|
||
</div>
|
||
<p style="font-size:.82rem;color:var(--ink-dim);line-height:1.4;margin:-2px 0 -2px;">Captured 3 days ago — warm tungsten cast on every fixture for a 19:00 unwind.</p>
|
||
<div class="props">
|
||
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg> 4 targets</span>
|
||
<span class="chip"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/></svg> Used by 2 automations</span>
|
||
<span class="chip">Updated 21 Apr</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn btn--primary" title="Activate"><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></button>
|
||
<button class="btn" title="Recapture"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="13" r="3"/><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"/></svg></button>
|
||
<button class="btn" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
<!-- Sync clock -->
|
||
<article class="card card--running" data-ch="amber">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">CLK · GEN <span class="id">// 110 BPM</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--on" title="Ticking"></span>
|
||
<span class="title">Master Tempo</span>
|
||
</div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">BPM</div>
|
||
<div class="readout__v readout__v--accent">110.00</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Phase</div>
|
||
<div class="readout__v">0.42</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Sub</div>
|
||
<div class="readout__v">1/16</div>
|
||
</div>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip">Drift · ±0.3ms</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Tap"><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="10"/><circle cx="12" cy="12" r="4"/></svg></button>
|
||
<button class="btn" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="section-title"><span class="num">03</span> Templates & assets</div>
|
||
|
||
<div class="grid">
|
||
|
||
<!-- Capture template with filter chain (config) -->
|
||
<article class="card" data-ch="cyan">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">TPL · CAPTURE <span class="id">// MSS</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--unknown" title="Template"></span>
|
||
<span class="title">Desktop · 60 fps · region</span>
|
||
</div>
|
||
<p style="font-size:.82rem;color:var(--ink-dim);line-height:1.4;margin:-2px 0 -2px;">Reusable capture preset for screen sources. 3 engine options pre-configured.</p>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Engine</div>
|
||
<div class="readout__v readout__v--accent">MSS</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Bind</div>
|
||
<div class="readout__v">3 keys</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Used by</div>
|
||
<div class="readout__v">5 sources</div>
|
||
</div>
|
||
</div>
|
||
<!-- Filter chain replaces the patterned config table — flow arrows -->
|
||
<div class="props" style="align-items:center;">
|
||
<span class="chip">crop</span>
|
||
<span style="color:var(--ch);opacity:.7;">→</span>
|
||
<span class="chip">downsample</span>
|
||
<span style="color:var(--ch);opacity:.7;">→</span>
|
||
<span class="chip">gamma</span>
|
||
<span style="color:var(--ch);opacity:.7;">→</span>
|
||
<span class="chip">color-correct</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Test"><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></button>
|
||
<button class="btn" title="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></button>
|
||
<button class="btn" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
<!-- Gradient — color preview is the headline -->
|
||
<article class="card" data-ch="signal">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">PALETTE <span class="id">// G-08</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--unknown" title="Stored"></span>
|
||
<span class="title">Aurora</span>
|
||
<span class="address" style="background:transparent;border-color:var(--ch-mix-22);color:var(--ch);">BUILTIN</span>
|
||
</div>
|
||
<!-- the gradient itself becomes the readout -->
|
||
<div style="height:32px;border-radius:var(--lux-r-sm);background:linear-gradient(90deg,#003e6b 0%,#0085c4 25%,#22d3ee 55%,#a855f7 80%,#ec4899 100%);box-shadow:inset 0 0 0 1px var(--line-soft),inset 0 1px 6px rgba(0,0,0,.4);position:relative;">
|
||
<span style="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;">5 STOPS · LOOP</span>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip">Used in 3 strips</span>
|
||
<span class="chip">HSL space</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="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></button>
|
||
<span class="swatch"></span>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- Asset -->
|
||
<article class="card" data-ch="cyan">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">ASSET · IMG <span class="id">// PNG</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--on" title="Available"></span>
|
||
<span class="title">cosmic-loop-001.png</span>
|
||
</div>
|
||
<!-- Image preview placeholder with ratio strip -->
|
||
<div style="height:64px;border-radius:var(--lux-r-sm);background:
|
||
radial-gradient(circle at 30% 40%,#a855f7 0,transparent 35%),
|
||
radial-gradient(circle at 70% 60%,#22d3ee 0,transparent 30%),
|
||
#07090e;
|
||
box-shadow:inset 0 0 0 1px var(--line-soft);"></div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Size</div>
|
||
<div class="readout__v">1920×1080</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Bytes</div>
|
||
<div class="readout__v">412 KB</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Download"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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="btn" title="Replace"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/></svg></button>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
<!-- Color strip (composite layered) -->
|
||
<article class="card card--running" data-ch="signal">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">STRIP · MAPPED <span class="id">// 144 PX</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--on" title="Mapping live"></span>
|
||
<span class="title">Cinema map · 144 px</span>
|
||
</div>
|
||
<!-- Live LED preview strip (per-pixel) -->
|
||
<div style="height:18px;border-radius:2px;background:linear-gradient(90deg,
|
||
#200035 0%, #5a1ca8 8%, #c14b8e 18%, #ff7a59 30%,
|
||
#ffd56b 40%, #95e7c9 50%, #00d8ff 65%, #2d6cdf 78%,
|
||
#1c2554 90%, #050816 100%);
|
||
box-shadow:inset 0 0 0 1px var(--line-soft);"></div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Source</div>
|
||
<div class="readout__v readout__v--accent">Cinematic</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Map</div>
|
||
<div class="readout__v">Letterbox</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Smooth</div>
|
||
<div class="readout__v">0.35</div>
|
||
</div>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip chip--link" title="Picture source"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/></svg> Display 2</span>
|
||
<span class="chip chip--link" title="CSPT chain">post-fx · 4 filters</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Test"><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></button>
|
||
<button class="btn" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
<!-- Game integration -->
|
||
<article class="card" data-ch="amber">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">GAME · CSGO <span class="id">// GSI</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--on" title="Listening"></span>
|
||
<span class="title">Counter-Strike 2</span>
|
||
</div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Port</div>
|
||
<div class="readout__v">3456</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Events</div>
|
||
<div class="readout__v">12 mapped</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Last</div>
|
||
<div class="readout__v">12s</div>
|
||
</div>
|
||
</div>
|
||
<div class="props">
|
||
<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>
|
||
<div class="footer">
|
||
<button class="btn" title="Test event"><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></button>
|
||
<button class="btn" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
<!-- HA / value source -->
|
||
<article class="card" data-ch="cyan">
|
||
<div class="drag" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
|
||
<div class="rail">
|
||
<div class="rail__bar"></div>
|
||
<div class="rail__label">VALUE · HA <span class="id">// SENSOR</span></div>
|
||
<div class="rail__notch"></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="title-row">
|
||
<span class="led led--on" title="Updating"></span>
|
||
<span class="title">Outdoor temp</span>
|
||
<span class="address">sensor.outdoor_temp</span>
|
||
</div>
|
||
<div class="readout">
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Now</div>
|
||
<div class="readout__v readout__v--accent">14.7°</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Range</div>
|
||
<div class="readout__v">8 — 22</div>
|
||
</div>
|
||
<div class="readout__cell">
|
||
<div class="readout__k">Tick</div>
|
||
<div class="readout__v">2s</div>
|
||
</div>
|
||
</div>
|
||
<div class="props">
|
||
<span class="chip">Bound to: brightness</span>
|
||
<span class="chip">Linear · clamped</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<button class="btn" title="Edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||
<span class="swatch"></span>
|
||
<button class="btn 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>
|
||
</article>
|
||
|
||
</div>
|
||
|
||
<section class="notes">
|
||
<h2>Coverage matrix — every card type</h2>
|
||
<ul style="grid-template-columns:repeat(auto-fit,minmax(240px,1fr));font-size:.82rem;">
|
||
<li><strong>Devices</strong> <code>data-device-id</code> · signal · rail <code>LED · OUT</code></li>
|
||
<li><strong>LED targets</strong> <code>data-target-id</code> · signal · rail <code>TARGET · OUT</code></li>
|
||
<li><strong>HA light targets</strong> <code>data-ha-target-id</code> · signal · rail <code>HA · OUT</code></li>
|
||
<li><strong>Color strips</strong> <code>data-css-id</code> · signal · rail <code>STRIP · MAPPED/COMPOSITE/MATH/EVENT</code></li>
|
||
<li><strong>CSPT</strong> <code>data-cspt-id</code> · signal · rail <code>TPL · STRIP</code></li>
|
||
<li><strong>Pattern templates</strong> <code>data-pattern-template-id</code> · signal · rail <code>TPL · PATTERN</code></li>
|
||
<li><strong>Picture sources</strong> <code>data-stream-id</code> · cyan · rail <code>SCREEN/IMG/VIDEO · IN</code></li>
|
||
<li><strong>Capture templates</strong> <code>data-template-id</code> · cyan · rail <code>TPL · CAPTURE</code></li>
|
||
<li><strong>PP templates</strong> <code>data-pp-template-id</code> · cyan · rail <code>TPL · POST-FX</code></li>
|
||
<li><strong>HA sources</strong> <code>data-id</code> · cyan · rail <code>HA · IN</code></li>
|
||
<li><strong>MQTT sources</strong> <code>data-id</code> · cyan · rail <code>MQTT · IN</code></li>
|
||
<li><strong>Weather sources</strong> <code>data-id</code> · cyan · rail <code>WX · IN</code></li>
|
||
<li><strong>Value sources</strong> <code>data-id</code> · cyan · rail <code>VALUE · HA/MQTT/MATH/SCHEDULE</code></li>
|
||
<li><strong>Audio capture sources</strong> <code>data-id</code> · magenta · rail <code>AUDIO · IN</code></li>
|
||
<li><strong>Audio processed sources</strong> <code>data-id</code> · magenta · rail <code>FFT · IN</code></li>
|
||
<li><strong>Audio templates</strong> <code>data-audio-template-id</code> · magenta · rail <code>TPL · AUDIO</code></li>
|
||
<li><strong>Audio processing tpls</strong> <code>data-apt-id</code> · magenta · rail <code>TPL · FFT</code></li>
|
||
<li><strong>Automations</strong> <code>data-automation-id</code> · violet · rail <code>SCENE · LOGIC</code></li>
|
||
<li><strong>Scene presets</strong> <code>data-scene-id</code> · violet · rail <code>SCENE · PRESET</code></li>
|
||
<li><strong>Sync clocks</strong> <code>data-sync-clock-id</code> · violet · rail <code>CLK · GEN</code></li>
|
||
<li><strong>Game integrations</strong> <code>data-gi-id</code> · amber · rail <code>GAME · <type></code></li>
|
||
<li><strong>Gradients</strong> <code>data-id</code> · signal · rail <code>PALETTE</code></li>
|
||
<li><strong>Assets</strong> <code>data-id</code> · cyan · rail <code>ASSET · IMG/AUD/VID</code></li>
|
||
</ul>
|
||
|
||
<h2 style="margin-top:42px;">What changes — and why</h2>
|
||
<ul>
|
||
<li><strong>Channel rail is always on.</strong> The dormant <code>--ch-*</code> palette becomes a top-edge bar with a silkscreened type label (<code>LED · OUT</code>, <code>FFT · IN</code>). Every card carries its identity; running cards just <em>brighten</em> rather than reveal.</li>
|
||
<li><strong>Status LED is honest.</strong> A 9px dot pulses for online, holds for offline (coral), warns for unknown (amber). The hand-rolled <code>health-dot</code> can be reused — only its glow needs lifting.</li>
|
||
<li><strong>LCD readout strip</strong> replaces ad-hoc <code>card-meta</code> badges. Three recessed cells with <em>Pixels / FPS / Lat</em> — tabular-nums, bordered hairline, faint inner shadow. Scannable in one fixation.</li>
|
||
<li><strong>Property chips are pill-shaped.</strong> Single chip system (<code>.chip</code>, <code>.chip--link</code>, <code>.chip--tag</code>) replaces the parallel <code>.card-meta</code> + <code>.stream-card-prop</code> styles. Crosslinks tint to the channel color on hover instead of generic gray.</li>
|
||
<li><strong>Drag handle and trash are persistent.</strong> 35% opacity at rest, 85% on hover. Touch and keyboard users see the affordances; ghost-buttons no longer require a mouse to discover.</li>
|
||
<li><strong>Footer seam.</strong> Rivet-style fastener dots and an inner gradient give the action row a "panel screwed onto the module" feel — separating controls from data without a hard rule.</li>
|
||
<li><strong>Brightness is a fader.</strong> Channel-tinted fill, recessed track, value as monospace readout — fits the metric language instead of looking like a generic <code><input type="range"></code>.</li>
|
||
<li><strong>Subtle grain.</strong> One <code>feTurbulence</code> SVG inlined; 4–5% opacity. Ends the "every dark UI is a flat black rectangle" feeling without costing performance.</li>
|
||
<li><strong>Light theme adapts cleanly.</strong> Channel hex values are already darkened in the existing tokens — the new design uses them unchanged.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
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));
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|