<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<title>Cybercore Theme Studio</title>
<style>
:root { color-scheme:dark;--bg:#090b12;--fg:#edf1f7;--panel:#111622;--line:#283243;--muted:#8591a4;--acid:#c8ff52;--pink:#ff4778;--cyan:#45e6dc;--purple:#a58bff;--orange:#ffad5c;--red:#ff5d68;--radius:8px;--font-ui:Inter,sans-serif;--font-mono:"JetBrains Mono",monospace;--font-display:Oxanium,sans-serif;--density:1;--dur-mid:.22s; }
*{box-sizing:border-box}html{scrollbar-color:var(--purple) var(--bg);scrollbar-width:thin}body{margin:0;min-height:100vh;background:var(--bg);color:var(--fg);font:13px/1.55 var(--font-ui);position:relative}button,input,select{font:inherit}button,.button{color:var(--fg);background:linear-gradient(145deg,color-mix(in srgb,var(--panel) 92%,white 3%),var(--panel));border:1px solid var(--line);border-radius:6px;padding:.62rem .9rem;cursor:pointer;text-decoration:none;letter-spacing:.02em;transition:all var(--dur-mid);white-space:nowrap}button:hover,.button:hover{border-color:var(--acid);color:var(--acid);box-shadow:0 0 22px color-mix(in srgb,var(--acid) 16%,transparent);transform:translateY(-1px)}button.primary{background:var(--acid);color:#11180a;border-color:var(--acid);font-weight:800}button.primary:hover{color:#11180a;box-shadow:0 0 28px color-mix(in srgb,var(--acid) 35%,transparent)}button.quiet{background:transparent}input,select{background:#0b1019;color:var(--fg);border:1px solid var(--line);border-radius:6px;padding:.64rem .72rem;width:100%;min-width:0}input:focus,select:focus{outline:1px solid var(--cyan);border-color:var(--cyan);box-shadow:0 0 0 3px color-mix(in srgb,var(--cyan) 11%,transparent)}
.bg-scanlines,.bg-glow{position:fixed;inset:0;pointer-events:none}.bg-scanlines{z-index:0;opacity:.32;background:repeating-linear-gradient(to bottom,#fff04 0 1px,transparent 1px 5px)}.bg-glow{z-index:0;background:radial-gradient(ellipse 52% 48% at 47% -10%,color-mix(in srgb,var(--purple) 17%,transparent),transparent 66%),radial-gradient(ellipse 30% 32% at 100% 0%,color-mix(in srgb,var(--cyan) 8%,transparent),transparent 72%)}header,main{position:relative;z-index:1}.topbar{height:68px;display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:0 24px;border-bottom:1px solid #ffffff12;background:#090b12e8;backdrop-filter:blur(18px);position:sticky;top:0;z-index:5}.brand{display:flex;align-items:center;gap:.72rem;letter-spacing:.13em;font:700 .82rem var(--font-mono);color:#f4f6fb}.brand-mark{display:grid;place-items:center;width:30px;height:30px;border:1px solid color-mix(in srgb,var(--acid) 65%,transparent);border-radius:8px;color:var(--acid);box-shadow:inset 0 0 14px color-mix(in srgb,var(--acid) 12%,transparent);font-size:1rem}.brand b{color:var(--acid)}.brand small{color:var(--muted);font:500 .66rem var(--font-mono);letter-spacing:.08em;margin-left:.18rem}.header-actions{display:flex;align-items:center;gap:.55rem}.header-actions button,.header-actions .button{font-size:.68rem;text-transform:uppercase}.main-shell{max-width:1800px;margin:auto;padding:22px 24px 40px}.intro{display:flex;align-items:center;justify-content:space-between;gap:1.5rem;margin:0 0 18px}.intro h1{margin:0;font:600 clamp(1.55rem,2.5vw,2.15rem)/1.12 var(--font-display);letter-spacing:-.045em}.intro p{margin:.4rem 0 0;color:var(--muted);font-size:.86rem}.intro .eyebrow,.eyebrow{color:var(--cyan);font:600 .65rem var(--font-mono);letter-spacing:.14em;text-transform:uppercase}.intro-actions{display:flex;gap:.55rem}.workspace{display:grid;grid-template-columns:250px minmax(360px,1fr) 360px;gap:14px;align-items:stretch;min-height:calc(100vh - 175px)}.panel{background:linear-gradient(160deg,color-mix(in srgb,var(--panel) 94%,white 2%),color-mix(in srgb,var(--bg) 72%,var(--panel)));border:1px solid #ffffff14;border-radius:10px;box-shadow:0 18px 55px #0005,inset 0 1px #ffffff08;min-width:0}.library{padding:14px;display:flex;flex-direction:column;gap:12px}.panel-title{display:flex;align-items:center;justify-content:space-between;gap:.6rem}.panel-title h2,.inspector-title{font-size:.79rem;text-transform:uppercase;letter-spacing:.09em;margin:0}.count{font:600 .65rem var(--font-mono);color:var(--muted);border:1px solid var(--line);border-radius:99px;padding:.2rem .48rem}.search-wrap{position:relative}.search-wrap:before{content:'⌕';position:absolute;left:11px;top:5px;color:var(--muted);font-size:1.2rem}.search-wrap input{padding-left:34px}.theme-picker-wrap{display:grid;gap:7px}.theme-row{display:flex;gap:6px}.theme-row select{flex:1}.theme-row button{padding:.55rem .65rem}.library-list{display:grid;gap:7px;overflow:auto;max-height:34vh;padding:2px}.theme-card{padding:10px;border:1px solid #ffffff10;border-radius:8px;background:#ffffff04;cursor:pointer;transition:.18s}.theme-card:hover,.theme-card.selected{border-color:color-mix(in srgb,var(--acid) 45%,var(--line));background:color-mix(in srgb,var(--acid) 5%,var(--panel))}.theme-card.selected{box-shadow:inset 2px 0 var(--acid)}.theme-card-head{display:flex;justify-content:space-between;align-items:center;gap:.5rem}.theme-card-name{font-weight:650;font-size:.8rem}.theme-card-tag{color:var(--muted);font:500 .57rem var(--font-mono);text-transform:uppercase}.theme-card-swatches{display:flex;gap:4px;margin-top:8px}.theme-card-swatches i{height:8px;flex:1;border-radius:4px;background:var(--swatch);border:1px solid #fff2}.library-actions{display:grid;grid-template-columns:1fr 1fr;gap:6px}.library-actions button{font-size:.62rem;padding:.55rem .35rem}.divider{height:1px;background:#ffffff12;margin:1px 0}.pack-controls{display:grid;gap:7px}.pack-controls .actions{display:grid;grid-template-columns:1fr;gap:6px}.actions{display:flex;flex-wrap:wrap;gap:.5rem;margin:.65rem 0}.pack-controls button{font-size:.64rem}.pack-controls .field{margin:0}.preview{padding:0;overflow:hidden;display:flex;flex-direction:column}.preview-top{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:14px 17px;border-bottom:1px solid #ffffff12}.preview-head{display:flex;align-items:center;gap:.7rem}.preview-head h2{font-size:.84rem;margin:0}.live-dot{width:7px;height:7px;border-radius:50%;background:var(--acid);box-shadow:0 0 11px var(--acid)}.view-tabs,.editor-tabs{display:flex;gap:3px;padding:3px;background:#080b12;border:1px solid #ffffff10;border-radius:7px}.view-tabs button,.editor-tabs button{border:0;background:transparent;padding:.42rem .65rem;color:var(--muted);font:600 .62rem var(--font-mono);text-transform:uppercase}.view-tabs button.active,.editor-tabs button.active{background:#1c2533;color:var(--fg);box-shadow:inset 0 0 0 1px #ffffff12}.canvas{flex:1;padding:22px;min-height:520px;background:radial-gradient(ellipse at center,color-mix(in srgb,var(--purple) 7%,transparent),transparent 65%);display:grid;place-items:center}.device{width:min(100%,760px);min-height:430px;border:1px solid #ffffff20;border-radius:12px;background:#0a0d14;box-shadow:0 25px 80px #0009;overflow:hidden;transition:.22s}.device-bar{height:35px;display:flex;align-items:center;gap:6px;padding:0 12px;background:#ffffff06;border-bottom:1px solid #ffffff10}.device-bar i{width:7px;height:7px;border-radius:50%;background:#ff5e65}.device-bar i:nth-child(2){background:#ffbd4b}.device-bar i:nth-child(3){background:#56d994}.device-url{margin:auto;width:44%;text-align:center;color:var(--muted);font:500 .58rem var(--font-mono);background:#0003;border-radius:99px;padding:.2rem}.sample-app{padding:22px;min-height:390px;background:var(--bg);color:var(--fg);font-family:var(--font-ui)}.sample-nav{display:flex;justify-content:space-between;align-items:center;padding-bottom:16px;border-bottom:1px solid var(--line);font:600 .65rem var(--font-mono);letter-spacing:.1em}.sample-nav strong{color:var(--acid)}.sample-nav span{color:var(--muted)}.sample-hero{padding:24px 0 18px}.sample-hero .eyebrow{color:var(--cyan)}.sample-hero h3{margin:.45rem 0;font:650 1.55rem/1.1 var(--font-display)}.sample-hero p{max-width:480px;color:var(--muted);margin:.55rem 0 1rem;font-size:.78rem}.sample-hero button{background:var(--acid);color:var(--bg);border:0;font-weight:700}.sample-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.sample-card{background:color-mix(in srgb,var(--bg) 55%,var(--panel));border:1px solid var(--line);border-radius:var(--radius);padding:calc(13px * var(--density,1));min-width:0}.sample-card h3{margin:.15rem 0 .4rem;font-size:.86rem}.sample-card p{color:var(--muted);font-size:.7rem;margin:.35rem 0}.mono{font-family:var(--font-mono)}.badge{display:inline-flex;align-items:center;gap:5px;border-radius:99px;padding:.22rem .55rem;color:var(--acid);background:color-mix(in srgb,var(--acid) 10%,transparent);border:1px solid color-mix(in srgb,var(--acid) 34%,transparent);font:.6rem var(--font-mono);text-transform:uppercase;letter-spacing:.07em}.palette-strip{display:flex;gap:4px;margin-top:.65rem}.palette-strip i{height:20px;flex:1;border-radius:4px;border:1px solid #ffffff22}.preview-foot{display:flex;justify-content:space-between;gap:1rem;padding:11px 16px;border-top:1px solid #ffffff12;color:var(--muted);font:.61rem var(--font-mono)}.preview-foot strong{color:var(--cyan);font-weight:500}.inspector{padding:14px;display:flex;flex-direction:column;gap:12px;overflow:auto}.inspector-head{display:flex;justify-content:space-between;align-items:center}.inspector-head .eyebrow{margin-bottom:3px}.editor-tabs{overflow:auto}.editor-tabs button{flex:1}.editor-pane{display:none}.editor-pane.active{display:block}.pane-intro{color:var(--muted);font-size:.71rem;margin:.1rem 0 .8rem}.field{display:grid;gap:.32rem;margin:.62rem 0}.field label,.eyebrow{font-size:.64rem;letter-spacing:.1em;text-transform:uppercase}.field label{color:#bdc7d5;font-family:var(--font-mono)}.field small{color:var(--muted);font-size:.65rem}.row{display:grid;grid-template-columns:1fr 1fr;gap:.55rem}.swatches{display:grid;grid-template-columns:1fr;gap:5px;margin-top:.5rem}.swatch{display:grid;grid-template-columns:minmax(85px,1fr) 34px minmax(80px,.8fr);align-items:center;gap:7px;color:#c5cedb;font:500 .65rem var(--font-mono);text-transform:capitalize;border-bottom:1px solid #ffffff09;padding:4px 0}.swatch input[type=color]{width:34px;height:29px;padding:3px;border-radius:5px}.swatch input[type=text]{font: .65rem var(--font-mono);padding:.45rem}.contrast-box{padding:9px 10px;background:#0a0e15;border:1px solid #ffffff12;border-radius:7px;margin-top:10px}#contrast{color:#b6c2d0;min-height:1.5rem;font-size:.68rem}#contrast.warning{color:var(--orange)}.quality-label{font:600 .6rem var(--font-mono);color:var(--cyan);letter-spacing:.08em;text-transform:uppercase}.status{min-height:1.25rem;color:var(--muted);font-size:.69rem;margin:.4rem 0}.status.error{color:var(--pink)}.status.ok{color:var(--acid)}.design-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 .6rem}.choice-row{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}.choice-row button{font-size:.6rem;padding:.55rem .3rem}.utility-stack{display:grid;gap:8px}.utility-stack .actions{margin:0;display:grid;grid-template-columns:1fr 1fr}.utility-stack button{font-size:.63rem;padding:.55rem .4rem}.pack-preview{display:grid;gap:.35rem;padding:.65rem;margin:.4rem 0;border:1px solid var(--line);border-radius:7px;background:#080b12;color:var(--muted);font-size:.66rem}.pack-preview[hidden]{display:none}.pack-swatches{display:flex;gap:.3rem}.pack-swatches i{width:1.2rem;height:1.2rem;border:1px solid var(--line);border-radius:4px}.statusline{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:8px 12px;border:1px solid #ffffff10;border-radius:7px;background:#ffffff04;color:var(--muted);font:.59rem var(--font-mono)}.statusline .ready{color:var(--acid)}
.theme-picker{position:relative;display:flex;align-items:center;gap:.45rem}.theme-label{color:var(--muted);font:600 .58rem var(--font-mono);letter-spacing:.1em}.theme-button{max-width:220px;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase;font-size:.66rem!important}.theme-menu{position:fixed;z-index:20;width:220px;max-height:min(70vh,560px);overflow-y:scroll;overscroll-behavior:contain;scrollbar-gutter:stable;background:var(--panel);border:1px solid var(--line);border-radius:3px;box-shadow:0 16px 40px #0008;padding:.3rem}.theme-menu[hidden]{display:none}.theme-group-label{padding:.45rem .55rem .25rem;color:var(--muted);font:600 .56rem var(--font-mono);letter-spacing:.08em;text-transform:uppercase}.theme-option{display:block;width:100%;padding:.42rem .55rem;border:0;border-radius:2px;background:transparent;text-align:left;color:var(--fg);font:500 .63rem var(--font-mono);text-transform:uppercase}.theme-option:hover,.theme-option[aria-selected=true]{background:color-mix(in srgb,var(--acid) 18%,var(--panel));color:var(--acid);box-shadow:none;transform:none}
.control-nav{display:flex;gap:6px;flex-wrap:wrap;margin:0 0 16px;padding:6px;border:1px solid #ffffff12;border-radius:9px;background:#0b1019}.control-nav button{font:600 .65rem var(--font-mono);text-transform:uppercase;color:var(--muted)}.control-nav button.active{color:var(--acid);border-color:color-mix(in srgb,var(--acid) 50%,var(--line));background:color-mix(in srgb,var(--acid) 8%,var(--panel))}.control-view[hidden]{display:none}.control-dashboard{display:grid;grid-template-columns:1.3fr repeat(3,1fr);gap:14px}.control-card{padding:18px}.control-card h2,.control-card h3{margin:0 0 .4rem;font:650 1rem var(--font-display)}.control-card h2{font-size:1.35rem}.control-card p{color:var(--muted);font-size:.76rem}.control-card strong{color:var(--cyan);font:600 .7rem var(--font-mono)}.resource-list{display:grid;gap:6px;margin-top:.8rem}.resource-list span{display:flex;justify-content:space-between;gap:1rem;padding:.45rem .55rem;border:1px solid #ffffff10;border-radius:5px;font:.68rem var(--font-mono)}.resource-list em{color:var(--acid);font-style:normal}.module-message{max-width:72rem;padding:2rem;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}.module-message strong{display:block;margin-bottom:.7rem;color:var(--cyan);font:650 1.15rem var(--font-display)}.module-toolbar{display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;margin:1rem 0}.module-toolbar button{font-size:.65rem}.module-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(17rem,1fr));gap:10px;margin-top:1rem}.resource-card{padding:1rem;border:1px solid #ffffff12;border-radius:8px;background:#ffffff04}.resource-card h3{margin:0 0 .35rem;color:var(--fg);font:650 .9rem var(--font-display)}.resource-card p{margin:.25rem 0;color:var(--muted);font-size:.72rem}.resource-meta{display:flex;gap:.35rem;flex-wrap:wrap;margin:.55rem 0}.resource-meta span{padding:.2rem .4rem;border:1px solid var(--line);border-radius:99px;color:var(--cyan);font:.58rem var(--font-mono)}.resource-card pre{max-height:13rem;overflow:auto;margin:.7rem 0 0;padding:.7rem;border:1px solid #ffffff12;border-radius:5px;background:#070a10;color:var(--acid);font:.65rem/1.5 var(--font-mono);white-space:pre-wrap}.resource-card .copy-resource{margin-top:.6rem;font-size:.62rem}.validation-list{display:grid;gap:8px;margin-top:1rem}.validation-list div{display:flex;justify-content:space-between;gap:1rem;padding:.7rem;border:1px solid #ffffff12;border-radius:6px;font:.7rem var(--font-mono)}.pass{color:var(--acid)}.fail{color:var(--pink)}.publish-output{margin-top:1rem;padding:1rem;border:1px solid var(--line);background:#070a10;font:.68rem/1.6 var(--font-mono);white-space:pre-wrap}@media(max-width:900px){.control-dashboard{grid-template-columns:1fr 1fr}.control-card:first-child{grid-column:1/-1}}@media(max-width:520px){.control-dashboard{grid-template-columns:1fr}}
.sample-hero .create-theme-action{display:inline-flex;align-items:center;padding:.62rem .9rem;border-radius:6px;background:var(--acid);color:var(--bg);font-size:.78rem;font-weight:700;text-decoration:none;transition:filter .18s,transform .18s}.sample-hero .create-theme-action:hover{filter:brightness(1.12);transform:translateY(-1px)}
.intro-actions{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:7px}.intro-actions button{font-size:.65rem}.intro-actions button:disabled{opacity:.38;cursor:not-allowed;transform:none;box-shadow:none}.draft-state{display:inline-flex;align-items:center;gap:6px;padding:.48rem .68rem;border:1px solid #ffffff18;border-radius:6px;color:var(--cyan);font:600 .59rem var(--font-mono);letter-spacing:.06em;white-space:nowrap}.draft-state:before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}.draft-state[data-state="dirty"]{color:var(--orange);border-color:color-mix(in srgb,var(--orange) 30%,transparent)}.draft-state[data-state="saved"]{color:var(--acid)}.preview-tools{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:wrap}.preview-tools>button{font-size:.57rem;padding:.48rem .55rem}.canvas{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px}.canvas-actions{width:min(100%,760px);display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid #ffffff15;border-radius:8px;background:#0a0d14}.canvas-actions>div{display:grid;gap:2px;flex:1}.canvas-actions strong{font:600 .58rem var(--font-mono);letter-spacing:.08em;color:var(--cyan)}.canvas-actions span{color:var(--muted);font-size:.66rem}.canvas-actions .create-theme-action{flex:none}.focus-hint{font:500 .56rem var(--font-mono);white-space:nowrap}.device.grayscale .sample-app{filter:grayscale(1)}.sample-app :focus-visible,.sample-app.show-focus :focus{outline:3px solid var(--cyan);outline-offset:3px;box-shadow:0 0 0 5px color-mix(in srgb,var(--cyan) 19%,transparent)}.sample-action{margin-top:.35rem;padding:.45rem .65rem;font-size:.67rem}.preview-foot strong{color:var(--cyan)}
.device.mobile{width:min(100%,390px);min-height:520px;border-radius:26px;padding:8px;background:#05070b}.device.mobile .device-bar{border-radius:20px 20px 0 0}.device.mobile .sample-app{min-height:480px}.device.mobile .sample-nav span{display:none}.device.mobile .sample-grid{grid-template-columns:1fr}.device.mobile .sample-hero h3{font-size:1.25rem}.device.terminal{border-radius:7px}.device.terminal .sample-app{font-family:var(--font-mono);background:color-mix(in srgb,var(--bg) 85%,black)}.device.terminal .sample-hero{border-left:2px solid var(--acid);padding-left:16px}.device.terminal .sample-hero h3{font-family:var(--font-mono);font-size:1.1rem}.device.terminal .sample-nav{color:var(--cyan)}
@media(max-width:1200px){.workspace{grid-template-columns:220px minmax(320px,1fr)}.inspector{grid-column:1/-1}}
@media(max-width:760px){.topbar{height:auto;min-height:62px;padding:10px 14px}.brand small{display:none}.header-actions .button{display:none}.main-shell{padding:16px 12px}.intro{display:block}.intro-actions{justify-content:flex-start;margin-top:12px}.workspace{grid-template-columns:1fr}.library{order:2}.preview{order:1}.inspector{order:3;grid-column:auto;display:flex}.editor-pane{display:none}.editor-pane.active{display:block}.canvas{padding:12px;min-height:400px}.device{min-height:390px}.sample-app{padding:15px}.sample-grid{gap:6px}.sample-card{padding:10px}.preview-tools{width:100%;justify-content:flex-start}.preview-tools .view-tabs{flex:1}.view-tabs button{padding:.4rem;flex:1}.canvas-actions{flex-wrap:wrap}.focus-hint{white-space:normal}.library-list{max-height:260px}}
@media(max-width:460px){.header-actions{gap:4px}.header-actions button{padding:.48rem .55rem;font-size:.58rem}.intro h1{font-size:1.5rem}.sample-grid,.row{grid-template-columns:1fr}.sample-hero{padding:17px 0 12px}.sample-hero h3{font-size:1.25rem}.preview-top{align-items:flex-start;flex-direction:column}.view-tabs{width:100%}.view-tabs button{flex:1}.swatch{grid-template-columns:minmax(75px,1fr) 32px minmax(78px,.8fr)}}
</style>
<style id="theme-css"></style>
<style id="draft-css"></style>
<style id="control-plane-catalog-css">
.resource-pagination{display:flex;align-items:center;justify-content:center;gap:.7rem;margin:1rem 0}.resource-pagination span{min-width:4rem;text-align:center;color:var(--cyan);font:600 .62rem var(--font-mono)}.resource-pagination button{font-size:.62rem;padding:.45rem .65rem}.resource-pagination button:disabled{opacity:.35;cursor:not-allowed;transform:none;box-shadow:none}
.resource-meta span{color:#55f6ff;border-color:#55f6ff88;background:#55f6ff12;text-shadow:0 0 7px #55f6ff99}.resource-meta span:nth-child(4n+1){color:#b6ff4a;border-color:#b6ff4a99;background:#b6ff4a15;text-shadow:0 0 7px #b6ff4aaa}.resource-meta span:nth-child(4n+2){color:#55f6ff;border-color:#55f6ff99;background:#55f6ff15}.resource-meta span:nth-child(4n+3){color:#ff62d2;border-color:#ff62d299;background:#ff62d215;text-shadow:0 0 7px #ff62d2aa}.resource-meta span:nth-child(4n){color:#b58cff;border-color:#b58cff99;background:#b58cff15;text-shadow:0 0 7px #b58cffaa}.resource-card .copy-resource{border-color:#55f6ff99;color:#55f6ff;background:#55f6ff12;box-shadow:inset 0 0 12px #55f6ff12,0 0 10px #55f6ff18;text-shadow:0 0 6px #55f6ff88}.resource-card .copy-resource:hover{color:#b6ff4a;border-color:#b6ff4a;box-shadow:0 0 18px #b6ff4a55;text-shadow:0 0 7px #b6ff4aaa}
.resource-card pre{color:#b5ffdc;background:linear-gradient(110deg,#071016,#0b0e18);border-color:#203348;box-shadow:inset 3px 0 var(--cyan);text-shadow:0 0 5px color-mix(in srgb,var(--cyan) 24%,transparent)}.resource-card pre::selection{background:color-mix(in srgb,var(--purple) 50%,transparent);color:var(--fg)}
.site-actions{display:flex;align-items:center;justify-content:space-between;gap:.7rem;flex-wrap:wrap;margin:1rem 0}.site-actions .actions{margin:0}.site-actions button{font-size:.62rem}.site-card{padding:1.15rem;border-color:#55f6ff30;background:linear-gradient(145deg,#08131a,#0b0d15 62%,#111021);box-shadow:inset 0 0 24px #55f6ff08,0 12px 32px #0005}.site-card:hover{border-color:#55f6ff88;box-shadow:inset 0 0 28px #55f6ff12,0 14px 38px #0008}.site-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:.7rem}.site-glyph{display:grid;place-items:center;width:2.2rem;height:2.2rem;border:1px solid #55f6ff88;border-radius:8px;color:#55f6ff;background:#55f6ff12;font:700 .7rem var(--font-mono);box-shadow:0 0 14px #55f6ff22}.site-card h3{margin:.1rem 0 .25rem}.site-repository{color:var(--muted);font:.62rem var(--font-mono);overflow-wrap:anywhere}.site-status{display:inline-flex;padding:.25rem .45rem;border-radius:99px;font:700 .56rem var(--font-mono);letter-spacing:.06em}.site-status.ready{color:#b6ff4a;border:1px solid #b6ff4a88;background:#b6ff4a12}.site-status.stale{color:#ffad5c;border:1px solid #ffad5c88;background:#ffad5c12}.site-status.missing{color:#ff62d2;border:1px solid #ff62d288;background:#ff62d212}.site-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.45rem;margin:.9rem 0}.site-detail{padding:.5rem;border:1px solid #ffffff12;border-radius:6px;background:#ffffff04}.site-detail small{display:block;color:var(--muted);font:.54rem var(--font-mono);text-transform:uppercase}.site-detail strong{display:block;margin-top:.15rem;color:var(--fg);font:.65rem var(--font-mono);overflow-wrap:anywhere}.site-contract{display:flex;gap:.3rem;flex-wrap:wrap;margin:.65rem 0}.site-contract span{padding:.25rem .4rem;border:1px solid #b58cff66;border-radius:99px;color:#d0c4ff;background:#b58cff10;font:.56rem var(--font-mono)}.site-issue{min-height:1.2rem;color:#ffad5c;font-size:.66rem}.site-card-actions{display:flex;gap:.4rem;flex-wrap:wrap;margin-top:.8rem}.site-card-actions button{font-size:.58rem;padding:.45rem .55rem}.site-dialog{width:min(640px,calc(100vw - 2rem));border:1px solid #55f6ff66;border-radius:10px;background:#0b1019;color:var(--fg);box-shadow:0 24px 80px #000c}.site-dialog::backdrop{background:#02040acc;backdrop-filter:blur(4px)}.site-dialog h2{margin:0;color:#55f6ff;font:650 1.1rem var(--font-display)}.site-dialog p{color:var(--muted);font-size:.72rem}.site-dialog-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:1rem}.site-step[hidden]{display:none}
.workspace[hidden]{display:none!important}.control-view{width:100%;max-width:1180px;margin:0 auto}.control-view.module-message{min-height:calc(100vh - 210px);padding:2rem 2.2rem}.control-only .main-shell{max-width:1280px}.control-only .intro{display:none}.control-only .control-view{margin-top:1rem}
.control-only button.primary{background:var(--cyan);border-color:var(--cyan);color:var(--bg);box-shadow:0 0 18px color-mix(in srgb,var(--cyan) 22%,transparent)}.control-only button.primary:hover{background:var(--pink);border-color:var(--pink);color:var(--bg);box-shadow:0 0 24px color-mix(in srgb,var(--pink) 28%,transparent)}.control-only .control-nav button.active{color:var(--cyan);border-color:color-mix(in srgb,var(--cyan) 55%,var(--line));background:color-mix(in srgb,var(--cyan) 9%,var(--panel))}.control-only .pass,.control-only .site-status.ready,.control-only .site-health strong{color:var(--cyan)}.control-only .site-status.ready,.control-only .site-health{border-color:color-mix(in srgb,var(--cyan) 55%,var(--line));background:color-mix(in srgb,var(--cyan) 8%,transparent)}.control-only .site-status.missing,.control-only .fail{color:var(--pink)}.control-only .site-status.missing{border-color:color-mix(in srgb,var(--pink) 55%,var(--line));background:color-mix(in srgb,var(--pink) 8%,transparent)}.control-only .site-status.stale{color:var(--orange)}
.dashboard-telemetry{display:grid;gap:14px;margin-top:14px}.dashboard-section{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.dashboard-section.wide{grid-template-columns:1.4fr 1fr 1fr}.dashboard-section.two{grid-template-columns:1fr 1fr}.telemetry-card{padding:15px;min-width:0}.telemetry-card h3{margin:0 0 .7rem;color:var(--cyan);font:650 .72rem var(--font-mono);letter-spacing:.08em;text-transform:uppercase}.telemetry-value{color:var(--fg);font:650 1.5rem var(--font-display)}.telemetry-sub{margin-top:.2rem;color:var(--muted);font-size:.67rem}.telemetry-list{display:grid;gap:.42rem}.telemetry-row{display:flex;justify-content:space-between;gap:.8rem;padding:.42rem .5rem;border:1px solid #ffffff10;border-radius:5px;background:#ffffff03;font:.62rem var(--font-mono)}.telemetry-row strong{color:var(--acid);font-weight:600;text-align:right}.telemetry-row strong.neutral{color:var(--muted)}.connector-card{position:relative;overflow:hidden}.connector-card:after{content:'';position:absolute;inset:auto -20% -65% 35%;height:100%;background:radial-gradient(ellipse,#55f6ff18,transparent 68%);pointer-events:none}.connector-status{display:inline-flex;margin-bottom:.7rem;padding:.25rem .45rem;border:1px solid #ffad5c66;border-radius:99px;color:#ffad5c;background:#ffad5c10;font:700 .55rem var(--font-mono);letter-spacing:.06em}.connector-status.connected{color:#b6ff4a;border-color:#b6ff4a88;background:#b6ff4a10}.site-health-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:7px}.site-health{padding:.55rem;border:1px solid #b6ff4a44;border-radius:6px;background:#b6ff4a08}.site-health strong{display:block;color:#b6ff4a;font:.58rem var(--font-mono);text-transform:uppercase}.site-health span{display:block;margin-top:.25rem;color:var(--muted);font-size:.62rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.telemetry-empty{color:var(--muted);font-size:.68rem}.dashboard-refresh{float:right;font-size:.58rem;padding:.35rem .5rem}@media(max-width:900px){.dashboard-section,.dashboard-section.wide,.dashboard-section.two{grid-template-columns:1fr 1fr}.site-health-grid{grid-template-columns:repeat(3,1fr)}}@media(max-width:520px){.dashboard-section,.dashboard-section.wide,.dashboard-section.two{grid-template-columns:1fr}.site-health-grid{grid-template-columns:1fr 1fr}}
</style>
<style id="holodrive-control-plane-css">
/* HoloDrive-inspired Control Plane surface: all color remains CYBERGRID-driven. */
body:is(.control-only, .theme-editor-visible) {
background:
radial-gradient(ellipse 55% 40% at 6% 0%, color-mix(in srgb, var(--cyan) 18%, transparent), transparent 62%),
radial-gradient(ellipse 60% 45% at 96% 6%, color-mix(in srgb, var(--pink) 18%, transparent), transparent 62%),
var(--bg);
overflow-x: hidden;
}
body:is(.control-only, .theme-editor-visible)::after {
content: "";
position: fixed;
z-index: 0;
left: -50%;
right: -50%;
bottom: -12vh;
height: 32vh;
pointer-events: none;
opacity: .24;
transform: perspective(340px) rotateX(68deg);
transform-origin: 50% 100%;
background:
repeating-linear-gradient(90deg, color-mix(in srgb, var(--pink) 45%, transparent) 0 1px, transparent 1px 64px),
repeating-linear-gradient(0deg, color-mix(in srgb, var(--cyan) 35%, transparent) 0 1px, transparent 1px 64px);
mask-image: linear-gradient(to bottom, transparent, #000 55%);
}
body:is(.control-only, .theme-editor-visible) .bg-glow {
background:
radial-gradient(ellipse 55% 40% at 6% 0%, color-mix(in srgb, var(--cyan) 18%, transparent), transparent 62%),
radial-gradient(ellipse 60% 45% at 96% 6%, color-mix(in srgb, var(--pink) 18%, transparent), transparent 62%),
radial-gradient(ellipse 80% 50% at 50% 108%, color-mix(in srgb, var(--purple) 30%, transparent), transparent 70%);
animation: control-plane-breathe 12s ease-in-out infinite;
}
body:is(.control-only, .theme-editor-visible) .bg-scanlines {
inset: auto 0 0;
height: 22vh;
z-index: 0;
opacity: .18;
background: repeating-linear-gradient(to bottom, #fff3 0 1px, transparent 1px 3px);
mask-image: linear-gradient(to bottom, transparent, #000 42%);
}
body:is(.control-only, .theme-editor-visible) .topbar {
background: color-mix(in srgb, var(--bg) 82%, transparent);
backdrop-filter: blur(10px);
border-bottom-color: var(--line);
}
body:is(.control-only, .theme-editor-visible) .topbar::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--cyan), var(--pink), transparent);
opacity: .7;
}
body:is(.control-only, .theme-editor-visible) .brand-mark {
border-radius: 9px;
color: var(--cyan);
border-color: var(--cyan);
box-shadow: 0 0 14px color-mix(in srgb, var(--cyan) 35%, transparent);
}
body:is(.control-only, .theme-editor-visible) .brand b,
body:is(.control-only, .theme-editor-visible) .eyebrow,
body:is(.control-only, .theme-editor-visible) .telemetry-card h3 {
color: var(--cyan);
text-shadow: 0 0 10px color-mix(in srgb, var(--cyan) 45%, transparent);
}
body:is(.control-only, .theme-editor-visible) .control-nav,
body:is(.control-only, .theme-editor-visible) .telemetry-card,
body:is(.control-only, .theme-editor-visible) .module-message,
body:is(.control-only, .theme-editor-visible) .workspace > .panel {
background: color-mix(in srgb, var(--panel) 78%, transparent);
border-color: var(--line);
border-radius: 12px;
backdrop-filter: blur(14px);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--purple) 20%, transparent),
0 24px 70px #0009,
0 0 40px color-mix(in srgb, var(--pink) 10%, transparent);
}
body:is(.control-only, .theme-editor-visible) .control-nav {
position: relative;
z-index: 1;
}
body:is(.control-only, .theme-editor-visible) .control-nav button {
border-radius: 9px;
transition: border-color .15s, box-shadow .15s, background .15s, transform .1s;
}
body:is(.control-only, .theme-editor-visible) .control-nav button:hover,
body:is(.control-only, .theme-editor-visible) .control-nav button.active {
border-color: var(--cyan);
color: var(--fg);
background: color-mix(in srgb, var(--cyan) 12%, transparent);
box-shadow: 0 0 12px color-mix(in srgb, var(--cyan) 50%, transparent);
}
body:is(.control-only, .theme-editor-visible) .telemetry-card {
position: relative;
overflow: hidden;
}
body:is(.control-only, .theme-editor-visible) .telemetry-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--cyan), var(--pink), transparent);
opacity: .7;
}
body:is(.control-only, .theme-editor-visible) .telemetry-card h3 {
letter-spacing: .12em;
}
body:is(.control-only, .theme-editor-visible) .telemetry-value {
text-shadow: 0 0 16px color-mix(in srgb, var(--pink) 45%, transparent);
}
body:is(.control-only, .theme-editor-visible) .telemetry-row {
background: color-mix(in srgb, var(--panel) 62%, transparent);
border-color: color-mix(in srgb, var(--line) 75%, transparent);
border-radius: 8px;
}
body:is(.control-only, .theme-editor-visible) .connector-status.connected {
color: var(--cyan);
border-color: var(--cyan);
background: color-mix(in srgb, var(--cyan) 12%, transparent);
box-shadow: 0 0 12px color-mix(in srgb, var(--cyan) 35%, transparent);
}
@media (prefers-reduced-motion: reduce) {
body:is(.control-only, .theme-editor-visible) .bg-glow { animation: none; }
body:is(.control-only, .theme-editor-visible)::after { display: none; }
}
@keyframes control-plane-breathe { 50% { opacity: .75; } }
</style>
</head>
<body>
<div class="bg-scanlines" aria-hidden="true"></div><div class="bg-glow" aria-hidden="true"></div>
<header class="topbar">
<div class="brand"><span class="brand-mark">✳</span><span>CYBERCORE <b>//</b> CONTROL PLANE <small>MASTER DESIGN SYSTEM</small></span></div>
<div class="header-actions"><div class="theme-picker"><span class="theme-label">THEME</span><button id="top-theme-button" class="theme-button" type="button" aria-haspopup="listbox" aria-expanded="false" aria-controls="top-theme-menu">LOADING THEMES…</button><div id="top-theme-menu" class="theme-menu" role="listbox" aria-label="Choose a Cybercore theme" hidden></div><select id="top-theme-picker" aria-hidden="true" tabindex="-1" hidden></select></div><button id="appearance" class="quiet" type="button" title="Change shared appearance">Loading mode…</button><a class="button" href="http://127.0.0.1:8080/">Cyberdeck Hub ↗</a></div>
</header>
<main class="main-shell">
<section class="intro"><div><div class="eyebrow">Theme engine · local workspace</div><h1>Build a look that travels everywhere.</h1><p>Design, test, and publish a unified visual language across your Cybercore apps.</p></div><div class="intro-actions"><span id="draft-state" class="draft-state" data-state="new">NEW DRAFT</span><button id="undo" type="button" title="Undo (Ctrl+Z)" disabled>↶ Undo</button><button id="redo" type="button" title="Redo (Ctrl+Shift+Z)" disabled>↷ Redo</button><button id="new-theme" type="button">+ New theme</button><button id="save-theme" class="primary" type="button">Save & apply</button></div></section>
<nav class="control-nav" aria-label="Control Plane modules">
<button class="active" type="button" data-control-view="themes">01 / Themes</button>
<button type="button" data-control-view="dashboard">00 / Dashboard</button>
<button type="button" data-control-view="creative">02 / Creative Forge</button>
<button type="button" data-control-view="components">03 / Components</button>
<button type="button" data-control-view="effects">04 / Effects</button>
<button type="button" data-control-view="vault">05 / Snippets + Assets</button>
<button type="button" data-control-view="sites">06 / Sites</button>
<button type="button" data-control-view="validation">07 / Validation</button>
<button type="button" data-control-view="publish">08 / Publish</button>
</nav>
<section id="control-dashboard" class="control-view" hidden aria-labelledby="control-dashboard-title">
<div class="dashboard-telemetry">
<div class="dashboard-section wide">
<article class="panel telemetry-card"><button id="refresh-telemetry" class="dashboard-refresh" type="button">Refresh</button><h3>CYBERCORE CONTROL PLANE</h3><h2 id="control-dashboard-title">One workspace.<br>Every surface.</h2><p id="workspace-status" class="telemetry-sub">Loading local telemetry…</p><div id="workspace-resources" class="resource-list"></div></article>
<article class="panel telemetry-card"><h3>RUNTIME</h3><div id="dashboard-uptime" class="telemetry-value">—</div><div class="telemetry-sub">Theme Studio uptime</div><div id="dashboard-runtime" class="telemetry-list"></div></article>
<article class="panel telemetry-card"><h3>SCHEMA</h3><div id="dashboard-version" class="telemetry-value">—</div><div class="telemetry-sub">Cybercore version</div><div id="dashboard-schema" class="telemetry-list"></div></article>
</div>
<div class="dashboard-section">
<article class="panel telemetry-card"><h3>THEMES</h3><div id="dashboard-theme-count" class="telemetry-value">—</div><div id="dashboard-active-theme" class="telemetry-sub">Active theme: —</div></article>
<article class="panel telemetry-card"><h3>RESOURCES</h3><div id="dashboard-resource-count" class="telemetry-value">—</div><div id="dashboard-resource-breakdown" class="telemetry-list"></div></article>
<article class="panel telemetry-card"><h3>SITES</h3><div id="dashboard-site-count" class="telemetry-value">—</div><div id="dashboard-site-breakdown" class="telemetry-list"></div></article>
<article class="panel telemetry-card"><h3>VALIDATION</h3><div id="dashboard-validation" class="telemetry-value">—</div><div id="dashboard-validation-detail" class="telemetry-sub">No run recorded</div></article>
</div>
<div class="dashboard-section two">
<article class="panel telemetry-card"><h3>SITE HEALTH MATRIX</h3><div id="dashboard-site-health" class="site-health-grid"></div></article>
<article class="panel telemetry-card"><h3>GOVERNANCE COVERAGE</h3><div id="dashboard-coverage" class="telemetry-list"></div></article>
</div>
<div class="dashboard-section">
<article class="panel telemetry-card connector-card"><h3>CLOUDFLARE</h3><div id="connector-cloudflare" class="connector-status">NOT CONFIGURED</div><p class="telemetry-sub">Web Analytics, Workers, DNS, cache, security, and service health.</p><div id="connector-metrics-cloudflare" class="telemetry-list"></div></article>
<article class="panel telemetry-card connector-card"><h3>GOOGLE</h3><div id="connector-google" class="connector-status">NOT CONFIGURED</div><p class="telemetry-sub">GA4, Search Console, and PageSpeed Insights.</p><div id="connector-metrics-google" class="telemetry-list"></div></article>
<article class="panel telemetry-card connector-card"><h3>GITHUB</h3><div id="connector-github" class="connector-status">NOT CONFIGURED</div><p class="telemetry-sub">Repositories, pull requests, Actions, releases, and rate limits.</p><div id="connector-metrics-github" class="telemetry-list"></div></article>
<article class="panel telemetry-card"><h3>API HEALTH</h3><div id="dashboard-response" class="telemetry-value">—</div><div id="dashboard-errors" class="telemetry-sub">No errors recorded</div></article>
</div>
<div class="dashboard-section two">
<article class="panel telemetry-card"><h3>RECENT CONTROL PLANE ACTIVITY</h3><div id="dashboard-activity" class="telemetry-list"><span class="telemetry-empty">No activity recorded yet.</span></div></article>
<article class="panel telemetry-card"><h3>SYNC POLICY</h3><div class="telemetry-list"><span class="telemetry-row"><span>External data</span><strong class="neutral">Explicit connector only</strong></span><span class="telemetry-row"><span>Unavailable metrics</span><strong class="neutral">NOT CONFIGURED</strong></span><span class="telemetry-row"><span>Privacy mode</span><strong>Local-first</strong></span></div></article>
</div>
<div class="dashboard-section two">
<article class="panel telemetry-card"><h3>RUNNING SERVICES</h3><div id="dashboard-services" class="telemetry-list"><span class="telemetry-empty">Loading services…</span></div></article>
<article class="panel telemetry-card"><h3>UPDATES</h3><div id="dashboard-updates" class="telemetry-list"><span class="telemetry-empty">Loading update state…</span></div></article>
</div>
<div class="dashboard-section">
<article class="panel telemetry-card"><h3>WARNINGS & ACTIONS</h3><div id="dashboard-warnings" class="telemetry-list"><span class="telemetry-empty">No warnings.</span></div></article>
</div>
</div>
</section>
<section id="control-placeholder" class="control-view module-message" hidden><strong id="module-title">Control Plane module</strong><span id="module-copy">This workspace is registered and ready for its editor.</span><div id="generic-module-toolbar" class="module-toolbar"><span id="module-count"></span><div><input id="resource-search" type="search" placeholder="Search resources…" aria-label="Search resources"><select id="resource-category" aria-label="Filter by category"><option value="">All categories</option></select><select id="resource-tag" aria-label="Filter by tag"><option value="">All tags</option></select><button id="validate-control-plane" type="button">Run validation</button><button id="publish-control-plane" type="button">Build publish plan</button></div></div><div id="site-actions" class="site-actions" hidden><span id="site-source" class="eyebrow">STARTER PROFILES · LOCAL ONLY</span><div class="actions"><button id="create-site-profile" class="primary" type="button">+ Create site profile</button><button id="validate-sites" type="button">Validate all sites</button></div></div><div id="resource-pagination" class="resource-pagination" hidden><button id="resource-prev" type="button">← Previous</button><span id="resource-page"></span><button id="resource-next" type="button">Next →</button></div><div id="module-resources" class="module-grid"></div><div id="validation-results" class="validation-list"></div><pre id="publish-output" class="publish-output" hidden></pre></section>
<dialog id="site-profile-dialog" class="site-dialog"><form method="dialog" id="site-profile-form"><h2>Create site profile</h2><p>Build a local profile in six governed steps. It will remain a draft until you explicitly publish it.</p><div class="site-step" data-site-step="1"><div class="field"><label for="site-repository">Repository</label><input id="site-repository" required placeholder="owner/repository"></div><div class="field"><label for="site-platform">Platform</label><select id="site-platform"><option>web</option><option>web-kit</option><option>rust-gui</option><option>terminal</option><option>vscode</option></select></div></div><div class="site-step" data-site-step="2" hidden><div class="field"><label for="site-theme">Active theme</label><input id="site-theme" value="cybercore-neon" required></div><div class="field"><label for="site-resources">Resources</label><input id="site-resources" placeholder="component.cyber-card, effect.neon-border"></div></div><div class="site-step" data-site-step="3" hidden><div class="field"><label for="site-motion">Motion and accessibility</label><select id="site-motion"><option>balanced</option><option>reduced</option><option>static</option></select><small>Profiles always retain keyboard-safe semantic fallbacks.</small></div><div class="field"><label for="site-fallback">Fallback mode</label><select id="site-fallback"><option>static-semantic</option><option>plain-content</option><option>safe-defaults</option></select></div></div><div class="site-dialog-actions"><button id="site-step-back" type="button" hidden>Back</button><button id="site-step-next" class="primary" type="button">Next</button><button id="site-create-submit" class="primary" type="submit" hidden>Create draft profile</button><button id="site-dialog-cancel" type="button">Cancel</button></div></form></dialog>
<div class="workspace">
<aside class="panel library" aria-label="Theme library">
<div class="panel-title"><h2>Library</h2><span id="theme-count" class="count">—</span></div>
<div class="search-wrap"><input id="theme-search" type="search" placeholder="Find a theme…" aria-label="Search themes"></div>
<div class="theme-picker-wrap"><label class="eyebrow" for="theme-picker">Selected theme</label><div class="theme-row"><select id="theme-picker"></select><button id="load-theme" type="button" title="Edit selected theme">Edit</button></div></div>
<div id="theme-list" class="library-list" aria-label="Available themes"></div>
<div class="library-actions"><button id="duplicate-theme" type="button">Duplicate</button><button id="delete-theme" type="button">Delete custom</button><button id="import-theme" type="button">Import theme</button><button id="export-theme" type="button">Export theme</button><input id="file" type="file" accept=".json,application/json" hidden></div>
<div class="divider"></div>
<div class="panel-title"><h2>Pack library</h2><span class="count">COLLECTIONS</span></div>
<div class="pack-controls"><div class="field"><label for="curated-pack">Curated collections</label><select id="curated-pack" aria-label="Choose a curated Cybercore theme pack"></select></div><div id="pack-preview" class="pack-preview" aria-live="polite" hidden></div><button id="install-curated-pack" type="button">+ Add collection</button><div class="actions"><button id="import-pack" type="button">Install pack JSON</button><button id="export-pack" type="button">Export family</button><input id="pack-file" type="file" accept=".json,.cyberpack.json,application/json" hidden></div><div class="field"><label for="pack-policy">Conflict handling</label><select id="pack-policy"><option value="reject">Reject duplicate IDs</option><option value="skip">Skip existing themes</option><option value="replace_custom">Replace custom matches</option></select></div><datalist id="theme-packs"></datalist></div>
</aside>
<section class="panel preview" aria-labelledby="preview-title">
<div class="preview-top"><div class="preview-head"><span class="live-dot"></span><div><div class="eyebrow">App reference preview</div><h2 id="preview-title">Cyberdeck Hub</h2></div></div><div class="preview-tools"><div class="view-tabs" role="tablist" aria-label="Choose app preview"><button class="active" data-app-preview="cyberdeck" type="button">Cyberdeck</button><button data-app-preview="dockspace" type="button">Dockspace</button><button data-app-preview="cyberdesk" type="button">Cyberdesk</button></div><button id="viewport-toggle" type="button" title="Switch preview viewport">Desktop</button><button id="color-proof" type="button" title="Preview in grayscale to check color-independent hierarchy">Color proof: full</button></div></div>
<div class="canvas"><div class="device"><div class="device-bar"><i></i><i></i><i></i><span class="device-url">127.0.0.1 · THEME SANDBOX</span></div><div class="sample-app"><div class="sample-nav"><strong id="preview-brand">CYBERDECK <span>//</span> HUB</strong><span id="preview-nav">HUB SCANS SYSTEM</span></div><div class="sample-hero"><div class="eyebrow" id="preview-kicker">SYSTEM OVERVIEW / 01</div><h3 id="preview-heading">Your command center.</h3><p id="preview-copy">Service health, diagnostics, and tools in one glance.</p></div><div id="preview-cards" class="sample-grid"></div></div></div><div class="canvas-actions"><div><strong>WORKING WITH THIS LOOK?</strong><span>Make a custom draft from the current preview.</span></div><button class="create-theme-action" id="create-from-preview" type="button">Make this a custom theme ↗</button><span class="focus-hint">Tab through controls to inspect focus rings.</span></div></div>
<div class="preview-foot"><span>LIVE DRAFT <strong>● UPDATING AS YOU EDIT</strong></span><span id="preview-theme-name">UNSAVED THEME</span></div>
</section>
<aside class="panel inspector" aria-label="Theme inspector">
<div class="inspector-head"><div><div class="eyebrow">Theme controls</div><h2 class="inspector-title">Inspector</h2></div><span class="count" id="edit-variant-label">DARK VARIANT</span></div>
<div class="editor-tabs" role="tablist" aria-label="Theme editing sections"><button class="active" data-editor-tab="identity" type="button">Identity</button><button data-editor-tab="palette" type="button">Color</button><button data-editor-tab="design" type="button">Style</button><button data-editor-tab="quality" type="button">Quality</button></div>
<section class="editor-pane active" data-editor-pane="identity"><p class="pane-intro">Name and organize this theme in your shared library.</p><div class="field"><label for="name">Theme name</label><input id="name" maxlength="96" placeholder="My custom theme"></div><div class="field"><label for="description">Description</label><input id="description" maxlength="256" placeholder="A short note about this visual direction"></div><div class="row"><div class="field"><label for="author">Designer</label><input id="author" maxlength="256" placeholder="Your name"></div><div class="field"><label for="family">Pack family</label><input id="family" list="theme-packs" maxlength="256" value="custom" placeholder="custom"></div></div></section>
<section class="editor-pane" data-editor-pane="palette"><p class="pane-intro">Tune each semantic color role. Changes appear across the live canvas.</p><div class="row"><div class="field"><label for="variant">Editing variant</label><select id="variant"><option value="dark">Dark</option><option value="light">Light</option></select></div><div class="field"><label>Palette tools</label><button id="palette-reset" type="button">Reset from active</button></div></div><div class="row"><div class="field"><label for="palette-harmony">Accent harmony</label><select id="palette-harmony"><option value="neon">Electric neon</option><option value="analogous">Analogous</option><option value="triadic">Triadic</option><option value="complementary">Complementary</option></select></div><div class="field"><label>Color generation</label><button id="palette-generate" type="button">Generate accents</button></div></div><div id="swatches" class="swatches"></div><div class="contrast-box"><div class="quality-label">Contrast checks</div><div id="contrast" role="status" aria-live="polite"></div></div></section>
<section class="editor-pane" data-editor-pane="design"><p class="pane-intro">Set typography and interaction character for every compatible app.</p><div class="design-grid"><div class="field"><label for="font-ui">Interface font</label><input id="font-ui" value="Inter" list="ui-fonts"><datalist id="ui-fonts"><option>Inter</option><option>System UI</option><option>Roboto</option><option>IBM Plex Sans</option><option>Atkinson Hyperlegible</option></datalist></div><div class="field"><label for="font-display">Display font</label><input id="font-display" value="Oxanium" list="display-fonts"><datalist id="display-fonts"><option>Oxanium</option><option>Space Grotesk</option><option>Rajdhani</option><option>Inter</option></datalist></div><div class="field"><label for="font-mono">Monospace font</label><input id="font-mono" value="JetBrains Mono" list="mono-fonts"><datalist id="mono-fonts"><option>JetBrains Mono</option><option>IBM Plex Mono</option><option>Fira Code</option><option>Source Code Pro</option></datalist></div><div class="field"><label for="density">Interface density</label><select id="density"><option>compact</option><option selected>comfortable</option><option>spacious</option></select></div><div class="field"><label for="corners">Corner profile</label><select id="corners"><option>sharp</option><option selected>soft</option><option>rounded</option></select></div><div class="field"><label for="motion">Motion</label><select id="motion"><option>full</option><option>reduced</option><option>none</option></select></div></div></section>
<section class="editor-pane" data-editor-pane="quality"><p class="pane-intro">Validate theme quality, manage files, and sync changes across apps.</p><div class="contrast-box"><div class="quality-label">Live quality report</div><div id="quality-summary">Updating checks as you edit…</div><div id="contrast-quality"></div></div><div class="field"><label>Theme file</label><div class="utility-stack"><div class="actions"><button id="export-theme-2" type="button">Export JSON</button><button id="import-theme-2" type="button">Import JSON</button></div><div class="actions"><button id="duplicate-theme-2" type="button">Duplicate</button><button id="delete-theme-2" type="button">Delete custom</button></div></div></div></section>
<div class="statusline"><span><span class="ready">●</span> SHARED CATALOG</span><span>LIVE SYNC ENABLED</span></div><p id="status" class="status" role="status" aria-live="polite"></p>
</aside>
</div>
</main>
<script>
const keys = ['bg','white','acid_green','hot_pink','purple','cyan','orange','red','panel','line','muted'];
const roles = {bg:'--bg',white:'--fg',acid_green:'--acid',hot_pink:'--pink',purple:'--purple',cyan:'--cyan',orange:'--orange',red:'--red',panel:'--panel',line:'--line',muted:'--muted'};
const el = id => document.getElementById(id);
let themes = [], activeId = '', editingId = null, sharedAppearance = 'dark', editMode = 'dark', variants = {}, debounce, validationRun = 0;
let editHistory=[],historyIndex=-1,historyTimer=0,baselineSnapshot='',suppressHistory=false,previewApp='cyberdeck',mobileViewport=false,grayscaleProof=false;
const previewProfiles={
cyberdeck:{name:'Cyberdeck Hub',brand:'CYBERDECK // HUB',nav:'HUB SCANS SYSTEM',kicker:'SYSTEM OVERVIEW / 01',heading:'Your command center.',copy:'Service health, diagnostics, and tools in one glance.',cards:[['SERVICE HEALTH','Systems online','12 tools reporting healthy status.','OPERATIONAL'],['QUICK ACCESS','Frequently used','Reach your daily tools from a single hub.','READY'],['RECENT ACTIVITY','Diagnostics complete','Hardware checks finished just now.','VIEW REPORT']]},
dockspace:{name:'Dockspace',brand:'DOCKSPACE // STACKS',nav:'STACKS RESOURCES ACTIVITY',kicker:'CONTAINER OPERATIONS / 01',heading:'Every stack in view.',copy:'Inspect Compose projects, container health, and recent activity.',cards:[['STACK STATUS','Core services','4 containers · all services healthy.','RUNNING'],['RESOURCE USE','Storage overview','Volumes and images at a glance.','2.4 GB'],['RECENT ACTIVITY','Deployment finished','The web stack started successfully.','JUST NOW']]},
cyberdesk:{name:'Cyberdesk',brand:'CYBERDESK // NOTES',nav:'NOTES TAGS SEARCH',kicker:'KNOWLEDGE WORKSPACE / 01',heading:'Your notes, connected.',copy:'Search technical notes and move between ideas without losing context.',cards:[['RECENT NOTE','Theme engine roadmap','Quality gates and app compatibility.','UPDATED'],['COLLECTION','System architecture','18 notes · 6 linked projects.','OPEN'],['SEARCH','Find across your vault','Search snippets, titles, and tags.','⌘ K']]}
};
function editorSnapshot(){return JSON.stringify({name:el('name').value,family:el('family').value,description:el('description').value,author:el('author').value,fontUi:el('font-ui').value,fontMono:el('font-mono').value,fontDisplay:el('font-display').value,density:el('density').value,corners:el('corners').value,motion:el('motion').value,editingId,editMode,variants:{...variants,[editMode]:paletteNow()},palette:paletteNow()});}
function updateDraftState(){const current=editorSnapshot(),dirty=baselineSnapshot!==''&¤t!==baselineSnapshot,pending=historyIndex>=0&¤t!==editHistory[historyIndex];const state=dirty?'dirty':editingId?'saved':'new';el('draft-state').dataset.state=state;el('draft-state').textContent=dirty?'UNSAVED CHANGES':editingId?'SAVED TO LIBRARY':'NEW DRAFT';el('undo').disabled=historyIndex<=0&&!pending;el('redo').disabled=pending||historyIndex<0||historyIndex>=editHistory.length-1;}
function resetHistory(){clearTimeout(historyTimer);const current=editorSnapshot();baselineSnapshot=current;editHistory=[current];historyIndex=0;updateDraftState();}
function scheduleHistory(){if(suppressHistory)return;updateDraftState();clearTimeout(historyTimer);historyTimer=setTimeout(()=>{const current=editorSnapshot();if(editHistory[historyIndex]!==current){editHistory=editHistory.slice(0,historyIndex+1);editHistory.push(current);historyIndex=editHistory.length-1;}updateDraftState();},220);}
function recordHistoryNow(){clearTimeout(historyTimer);const current=editorSnapshot();if(editHistory[historyIndex]!==current){editHistory=editHistory.slice(0,historyIndex+1);editHistory.push(current);historyIndex=editHistory.length-1;}updateDraftState();}
function undoDraft(){recordHistoryNow();restoreHistory(historyIndex-1);}
function redoDraft(){recordHistoryNow();restoreHistory(historyIndex+1);}
function restoreHistory(index){if(index<0||index>=editHistory.length)return;historyIndex=index;const state=JSON.parse(editHistory[index]);suppressHistory=true;editingId=state.editingId;editMode=state.editMode;variants=state.variants;el('name').value=state.name;el('family').value=state.family;el('description').value=state.description;el('author').value=state.author;el('font-ui').value=state.fontUi;el('font-mono').value=state.fontMono;el('font-display').value=state.fontDisplay;el('density').value=state.density;el('corners').value=state.corners;el('motion').value=state.motion;el('variant').value=editMode;fillPalette(state.palette);suppressHistory=false;renderThemeList();updateDraftState();}
function confirmDiscardDraft(){return editorSnapshot()===baselineSnapshot||confirm('Discard the unsaved theme changes and continue?');}
function renderAppPreview(app){previewApp=app;const profile=previewProfiles[app]||previewProfiles.cyberdeck;el('preview-title').textContent=profile.name;el('preview-brand').textContent=profile.brand;el('preview-nav').textContent=profile.nav;el('preview-kicker').textContent=profile.kicker;el('preview-heading').textContent=profile.heading;el('preview-copy').textContent=profile.copy;const cards=profile.cards.map(([label,title,copy,badge])=>{const article=document.createElement('article');article.className='sample-card';const eyebrow=document.createElement('div');eyebrow.className='eyebrow';eyebrow.textContent=label;const heading=document.createElement('h3');heading.textContent=title;const status=document.createElement('span');status.className='badge';status.textContent=badge;heading.append(' ',status);const body=document.createElement('p');body.textContent=copy;article.append(eyebrow,heading,body);return article;});const action=document.createElement('article');action.className='sample-card';const actionLabel=document.createElement('div');actionLabel.className='eyebrow';actionLabel.textContent='INTERACTION STATES';const actionHeading=document.createElement('h3');actionHeading.textContent='Keyboard and pointer';const actionCopy=document.createElement('p');actionCopy.textContent='Check that controls stay visible and distinct.';const sampleButton=document.createElement('button');sampleButton.type='button';sampleButton.className='sample-action';sampleButton.setAttribute('aria-pressed','false');sampleButton.textContent='Preview control';action.append(actionLabel,actionHeading,actionCopy,sampleButton);cards.push(action);const palette=document.createElement('article');palette.className='sample-card';const paletteLabel=document.createElement('div');paletteLabel.className='eyebrow';paletteLabel.textContent='COLOR SYSTEM';const paletteHeading=document.createElement('h3');paletteHeading.textContent='Semantic palette';const strip=document.createElement('div');strip.id='palette-strip';strip.className='palette-strip';palette.append(paletteLabel,paletteHeading,strip);cards.push(palette);el('preview-cards').replaceChildren(...cards);document.querySelectorAll('[data-app-preview]').forEach(button=>button.classList.toggle('active',button.dataset.appPreview===app));document.querySelector('.device').classList.toggle('mobile',mobileViewport);document.querySelector('.device').classList.toggle('grayscale',grayscaleProof);}
function paletteNow() { return Object.fromEntries(keys.map(key => {const value=el('hex-' + key).value.trim().replace(/^#/,'');return [key,/^[0-9a-f]{6}$/i.test(value)?value:el('color-' + key).value.slice(1)];})); }
function fillPalette(palette) { keys.forEach(key => { const value=((palette && palette[key]) || '888888').replace('#','');el('color-' + key).value='#'+value;el('hex-' + key).value=value.toUpperCase(); }); preview(); }
function currentDocument() {
variants[editMode] = paletteNow();
const name = el('name').value.trim() || 'Untitled theme';
const id = editingId || name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'').slice(0,64) || 'custom-theme';
const palette = variants.dark || paletteNow();
const savedVariants = variants.light ? {light:variants.light} : {};
return {format_version:1, metadata:{id,name,family:el('family').value.trim() || 'custom',description:el('description').value,author:el('author').value}, palette, variants:savedVariants,
design:{font_ui:el('font-ui').value,font_mono:el('font-mono').value,font_display:el('font-display').value,density:el('density').value,corners:el('corners').value,motion:el('motion').value}};
}
function preview() {
const validation = ++validationRun;
const p = paletteNow(), css = [];
el('preview-theme-name').textContent=(el('name').value.trim() || 'UNSAVED THEME').toUpperCase();
for (const key of keys) css.push(roles[key]+':#'+p[key].replace('#',''));
const safeFont = value => /^[A-Za-z0-9 _-]{1,64}$/.test(value) ? value : 'Inter';
css.push('--font-ui:"'+safeFont(el('font-ui').value)+'",sans-serif','--font-mono:"'+safeFont(el('font-mono').value)+'",monospace','--font-display:"'+safeFont(el('font-display').value)+'",sans-serif');
css.push('--radius:'+({sharp:'2px',soft:'10px',rounded:'18px'}[el('corners').value] || '10px'));
css.push('--density:'+({compact:'.85',comfortable:'1',spacious:'1.15'}[el('density').value] || '1'));
css.push('--dur-mid:'+({full:'.24s',reduced:'.08s',none:'0s'}[el('motion').value] || '.24s'));
el('draft-css').textContent=':root{'+css.join(';')+'}';
el('palette-strip').replaceChildren(...keys.slice(2,8).map(key => {const swatch=document.createElement('i');swatch.style.backgroundColor='#'+p[key].replace('#','');swatch.title=key;return swatch;}));
clearTimeout(debounce); debounce=setTimeout(()=>validateDraft(validation),180);
}
async function request(url, options) {
const response = await fetch(url, options);
const data = response.status === 204 ? null : await response.json().catch(()=>null);
if (!response.ok) throw new Error(data?.error || 'Request failed ('+response.status+')');
return data;
}
function message(text, kind='') { el('status').textContent=text; el('status').className='status '+kind; }
async function validateDraft(run) {
if (run !== validationRun) return;
try {
const report = await request('/api/themes/validate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(currentDocument())});
if (run !== validationRun) return;
const result = editMode === 'light' ? (report.light_contrast || report.contrast) : report.contrast;
const failed = (result?.checks || []).filter(check=>!check.passes);
el('contrast').classList.toggle('warning',failed.length>0);
const fallback = report.quality?.has_explicit_light_variant === false ? ' Light mode currently falls back to the base palette.' : '';
el('contrast').textContent = failed.length ? 'Contrast needs attention: '+failed.map(check=>check.label+' '+check.ratio.toFixed(2)+':1').join(', ')+fallback : 'Common text, focus, border, and accent contrast checks pass.'+fallback;
const checks=report.quality?.checks || [], passed=checks.filter(check=>check.passes).length;
el('quality-summary').textContent=passed+'/'+checks.length+' contrast checks pass · '+(report.quality?.has_explicit_light_variant?'Light variant designed':'Light variant not defined');
el('quality-summary').style.color=failed.length?'var(--orange)':'var(--acid)';
el('contrast-quality').textContent=failed.length?failed.map(check=>check.appearance+' · '+check.label+' '+check.ratio.toFixed(2)+':1').join(' / '):'No failed contrast checks across the available variants.';
} catch (error) { if (run === validationRun) {el('contrast').classList.remove('warning');el('contrast').textContent=error.message;} }
}
function renderThemeList() {
const query=el('theme-search').value.trim().toLowerCase();
const visible=themes.filter(theme=>(theme.document.metadata.name+' '+theme.document.metadata.family+' '+theme.id).toLowerCase().includes(query));
el('theme-count').textContent=themes.length+' THEMES';
el('theme-list').replaceChildren(...visible.map(theme=>{
const card=document.createElement('div');card.className='theme-card'+(theme.id===el('theme-picker').value?' selected':'');card.tabIndex=0;card.setAttribute('role','button');card.setAttribute('aria-label','Edit '+theme.document.metadata.name);
const head=document.createElement('div');head.className='theme-card-head';const name=document.createElement('span');name.className='theme-card-name';name.textContent=theme.document.metadata.name;const tag=document.createElement('span');tag.className='theme-card-tag';const wallpaperCount=theme.document.metadata.wallpapers?.length || 0;tag.textContent=(theme.builtin?'BUILT IN':'CUSTOM')+(wallpaperCount?' · '+wallpaperCount+' WALLPAPERS':'');head.append(name,tag);
const swatches=document.createElement('div');swatches.className='theme-card-swatches';for(const key of ['bg','panel','acid_green','hot_pink','cyan']){const chip=document.createElement('i');chip.style.setProperty('--swatch','#'+theme.document.palette[key]);swatches.append(chip);}card.append(head,swatches);
const choose=()=>{if(!confirmDiscardDraft())return;el('theme-picker').value=theme.id;el('theme-picker').dispatchEvent(new Event('change'));editDocument(theme.document,theme.builtin?null:theme.id);resetHistory();renderThemeList();};card.addEventListener('click',choose);card.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();choose();}});return card;
}));
}
async function refreshLibrary(preferred=activeId) {
const data=await request('/api/themes'); themes=data.themes; activeId=data.active; sharedAppearance=data.appearance;
el('appearance').textContent=sharedAppearance+' appearance';
const families=[...new Set(themes.map(theme=>theme.document.metadata.family || 'default'))].sort((a,b)=>a.localeCompare(b));
el('theme-packs').replaceChildren(...families.map(family=>{const option=document.createElement('option');option.value=family;return option;}));
el('theme-picker').replaceChildren(...families.map(family=>{const group=document.createElement('optgroup');group.label=family+' pack';for(const theme of themes.filter(item=>(item.document.metadata.family || 'default')===family)){const option=document.createElement('option');option.value=theme.id;option.textContent=theme.document.metadata.name+(theme.builtin?' · built-in':' · custom');group.append(option);}return group;}));
el('top-theme-picker').replaceChildren(...families.map(family=>{const group=document.createElement('optgroup');group.label=family+' pack';for(const theme of themes.filter(item=>(item.document.metadata.family || 'default')===family)){const option=document.createElement('option');option.value=theme.id;option.textContent=theme.document.metadata.name+(theme.builtin?' · built-in':' · custom');group.append(option);}return group;}));
el('theme-picker').value=themes.some(theme=>theme.id===preferred)?preferred:activeId;
el('top-theme-picker').value=themes.some(theme=>theme.id===preferred)?preferred:activeId;
rebuildTopThemeMenu();
renderThemeList();
await applyTheme(activeId);
}
function rebuildTopThemeMenu() {
const menu=el('top-theme-menu'), button=el('top-theme-button'), select=el('top-theme-picker');
menu.replaceChildren();
select.querySelectorAll('optgroup').forEach(group=>{
const heading=document.createElement('div'); heading.className='theme-group-label'; heading.textContent=group.label; menu.append(heading);
group.querySelectorAll('option').forEach(source=>{
const option=document.createElement('button'); option.type='button'; option.className='theme-option'; option.setAttribute('role','option'); option.dataset.value=source.value; option.textContent=source.textContent;
option.addEventListener('click',()=>{select.value=source.value;select.dispatchEvent(new Event('change',{bubbles:true}));closeTopThemeMenu();button.focus();});
menu.append(option);
});
});
syncTopThemeMenu();
}
function syncTopThemeMenu() {
const select=el('top-theme-picker'), button=el('top-theme-button');
const selected=select.selectedOptions[0];
button.textContent=selected?.textContent || 'LOADING THEMES…';
el('top-theme-menu').querySelectorAll('.theme-option').forEach(option=>option.setAttribute('aria-selected',String(option.dataset.value===select.value)));
}
function closeTopThemeMenu() {
const menu=el('top-theme-menu'); menu.hidden=true; el('top-theme-button').setAttribute('aria-expanded','false');
}
async function applyTheme(id) {
const response=await fetch('/api/css/'+encodeURIComponent(id)+'?appearance='+sharedAppearance,{headers:{Accept:'text/css'}});
if(!response.ok) throw new Error('Theme stylesheet unavailable');
el('theme-css').textContent=await response.text(); el('draft-css').textContent=''; activeId=id;
document.documentElement.dataset.cybercoreTheme=id;
const selected=themes.find(theme=>theme.id===id);if(selected)el('preview-theme-name').textContent=selected.document.metadata.name.toUpperCase();
document.documentElement.style.colorScheme=sharedAppearance;
}
function editDocument(document, id=null) {
editingId = id;
variants=JSON.parse(JSON.stringify(document.variants || {})); variants.dark=document.palette; editMode='dark'; el('variant').value='dark';
el('name').value=document.metadata.name || ''; el('family').value=document.metadata.family || 'custom'; el('description').value=document.metadata.description || ''; el('author').value=document.metadata.author || '';
el('font-ui').value=document.design?.font_ui || 'Inter'; el('font-mono').value=document.design?.font_mono || 'JetBrains Mono'; el('font-display').value=document.design?.font_display || 'Oxanium';
el('density').value=document.design?.density || 'comfortable'; el('corners').value=document.design?.corners || 'sharp'; el('motion').value=document.design?.motion || 'full';
fillPalette(document.palette || document);
}
function newTheme() {
editingId = null;
variants={}; editMode='dark'; el('variant').value='dark'; el('name').value=''; el('family').value='custom'; el('description').value=''; el('author').value='';
el('font-ui').value='Inter'; el('font-mono').value='JetBrains Mono'; el('font-display').value='Oxanium'; el('density').value='comfortable'; el('corners').value='sharp'; el('motion').value='full';
const active=themes.find(theme=>theme.id===activeId); fillPalette(active?.document.palette); resetHistory();message('New theme draft.');
}
function duplicateTheme() {
if(!confirmDiscardDraft())return;
const source=themes.find(theme=>theme.id===el('theme-picker').value);
if (!source) { message('Choose a theme to duplicate first.','error'); return; }
editDocument(source.document);
const base=(source.document.metadata.name || source.id)+' Copy';
let name=base, suffix=2;
const ids=new Set(themes.map(theme=>theme.id));
const slug=value=>value.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'').slice(0,64) || 'custom-theme';
while(ids.has(slug(name))) name=base+' '+suffix++;
el('name').value=name;
editingId=null;
preview();
resetHistory();message('Editing a new copy of '+source.document.metadata.name+'. Save when ready.','ok');
}
function createThemeFromPreview() {
const draft=currentDocument();
const sourceName=el('name').value.trim() || themes.find(theme=>theme.id===activeId)?.document.metadata.name || 'Custom theme';
const base=sourceName+' Custom', slug=value=>value.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'').slice(0,64) || 'custom-theme';
const ids=new Set(themes.map(theme=>theme.id)),root=slug(base);let name=base,id=root,suffix=2;
while(ids.has(id)){const tail='-'+suffix;id=root.slice(0,64-tail.length)+tail;name=base+' '+suffix++;}
draft.metadata.name=name;draft.metadata.id=id;editingId=null;
editDocument(draft);
document.querySelector('[data-editor-tab="identity"]').click();
resetHistory();el('name').focus();message('Custom draft created from the current preview. Name it, then save and apply.','ok');
}
async function saveTheme() {
try { const result=await request('/api/themes',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(currentDocument())}); editingId=result.id; await refreshLibrary(result.id);resetHistory();message('Saved and applied '+result.name+'.','ok'); }
catch(error) { message(error.message,'error'); }
}
async function deleteTheme() {
const id=el('theme-picker').value, theme=themes.find(item=>item.id===id);
if (!theme || theme.builtin) { message('Built-in themes cannot be deleted.','error'); return; }
try { await request('/api/themes/'+encodeURIComponent(id),{method:'DELETE'}); await refreshLibrary(); message('Custom theme deleted.','ok'); }
catch(error) { message(error.message,'error'); }
}
async function changeAppearance() {
const next=sharedAppearance==='dark'?'light':'dark';
try { await request('/api/appearance/'+next,{method:'POST'}); sharedAppearance=next; el('appearance').textContent=next+' appearance'; await applyTheme(activeId); }
catch(error) { message(error.message,'error'); }
}
function changeVariant() {
variants[editMode]=paletteNow(); editMode=el('variant').value;
fillPalette(editMode==='light' ? (variants.light || variants.dark) : (variants.dark || variants.light));
}
function exportTheme() {
const themeDocument=currentDocument(), blob=new Blob([JSON.stringify(themeDocument,null,2)],{type:'application/json'}), link=document.createElement('a');
link.href=URL.createObjectURL(blob); link.download=themeDocument.metadata.id+'.json'; link.click(); URL.revokeObjectURL(link.href);
}
async function importTheme(file) {
if (!file) return;
try { const data=JSON.parse(await file.text());if(!confirmDiscardDraft())return;editDocument(data);scheduleHistory();message('Imported '+file.name+'. Review and save when ready.','ok'); }
catch(error) { message('Could not import theme: '+error.message,'error'); }
el('file').value='';
}
async function exportPack() {
let family=el('family').value.trim();
if(!themes.some(theme=>(theme.document.metadata.family || 'default').toLowerCase()===family.toLowerCase())) family=themes.find(theme=>theme.id===el('theme-picker').value)?.document.metadata.family || '';
if(!family){message('Choose a family pack to export.','error');return;}
try {
const response=await fetch('/api/packs/export?family='+encodeURIComponent(family));
if(!response.ok)throw new Error(response.status===404?'No themes found for this pack.':'Pack export failed ('+response.status+')');
const pack=await response.json(), blob=new Blob([JSON.stringify(pack,null,2)],{type:'application/json'}), link=document.createElement('a');
link.href=URL.createObjectURL(blob);link.download=pack.metadata.id+'.cyberpack.json';link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000);
message('Exported '+pack.metadata.name+' ('+pack.themes.length+' themes).','ok');
} catch(error) { message(error.message,'error'); }
}
async function importPack(file) {
if(!file)return;
try {
const pack=JSON.parse(await file.text());
const preview=await request('/api/packs/validate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(pack)});
showPackPreview(preview.metadata || pack.metadata);
const policy=el('pack-policy').value, conflicts=preview.conflicts.custom.length+preview.conflicts.builtin.length;
if(conflicts && policy==='reject') { message('Pack validated, but '+conflicts+' theme IDs already exist. Choose a conflict policy before installing.','error');return; }
const policyLabel={skip:'skip existing themes',replace_custom:'replace matching custom themes',reject:'reject conflicts'}[policy];
if(!confirm('Install '+pack.metadata.name+' with '+preview.theme_count+' themes? Policy: '+policyLabel+'.'))return;
const result=await request('/api/packs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({pack,policy})});
await refreshLibrary();
message('Installed '+result.name+': '+result.imported.length+' added, '+result.skipped.length+' skipped.','ok');
} catch(error) { message('Could not install pack: '+error.message,'error'); }
finally { el('pack-file').value=''; }
}
async function loadCuratedLibrary() {
const library=await request('/api/library');
curatedPacks=library.packs;
el('curated-pack').replaceChildren(...curatedPacks.map(pack=>{const option=document.createElement('option');option.value=pack.id;option.textContent=pack.name+' · '+pack.theme_count+' themes';option.title=pack.description;return option;}));
showPackPreview(curatedPacks[0]?.metadata);
}
const studioThemeEvents=new EventSource('/api/events');
studioThemeEvents.addEventListener('theme-change',()=>refreshLibrary(el('theme-picker').value).catch(error=>console.debug('Shared theme refresh deferred',error)));
let curatedPacks=[];
function showPackPreview(metadata) {
const target=el('pack-preview');
if(!metadata){target.hidden=true;target.replaceChildren();return;}
target.hidden=false;
const title=document.createElement('strong');title.textContent=(metadata.name || 'Theme pack')+' · v'+(metadata.version || '1.0.0');
const details=document.createElement('span');details.textContent=[metadata.author || 'Unknown author',metadata.license || 'License unspecified','Cybercore '+(metadata.compatibility || 'compatibility unspecified')].join(' · ');
const swatches=document.createElement('span');swatches.className='pack-swatches';swatches.setAttribute('aria-label','Pack palette preview');
for(const color of (metadata.preview || []).slice(0,5)){if(!/^#?[0-9a-f]{6}$/i.test(color))continue;const swatch=document.createElement('i');swatch.style.backgroundColor='#'+color.replace(/^#/,'');swatch.title=color;swatches.append(swatch);}
target.replaceChildren(title,details,swatches);
}
el('curated-pack').addEventListener('change',()=>showPackPreview(curatedPacks.find(pack=>pack.id===el('curated-pack').value)?.metadata));
async function installCuratedPack() {
const id=el('curated-pack').value;
if(!id)return;
const pack=el('curated-pack').selectedOptions[0]?.textContent || id;
if(!confirm('Add '+pack+' as a custom collection? Existing themes and your active selection stay unchanged.'))return;
try {
const result=await request('/api/library/'+encodeURIComponent(id)+'/install',{method:'POST'});
await refreshLibrary();
message('Added curated-'+id+': '+result.imported.length+' themes copied to your library.','ok');
} catch(error) { message('Could not add curated collection: '+error.message,'error'); }
}
function buildSwatches() {
el('swatches').replaceChildren(...keys.map(key=>{const label=document.createElement('label');label.className='swatch';const title=document.createElement('span');title.textContent=key.replace('_',' ');const color=document.createElement('input');color.type='color';color.id='color-'+key;color.setAttribute('aria-label',key.replace('_',' ')+' color picker');const hex=document.createElement('input');hex.type='text';hex.id='hex-'+key;hex.value='888888';hex.maxLength=7;hex.autocomplete='off';hex.spellcheck=false;hex.setAttribute('aria-label',key.replace('_',' ')+' hex color');color.addEventListener('input',()=>{hex.value=color.value.slice(1).toUpperCase();preview();});hex.addEventListener('input',()=>{const value=hex.value.trim().replace(/^#/,'');if(/^[0-9a-f]{6}$/i.test(value)){color.value='#'+value;preview();}});label.append(title,color,hex);return label;}));
}
buildSwatches();renderAppPreview('cyberdeck');
document.querySelectorAll('[data-editor-tab]').forEach(tab=>tab.addEventListener('click',()=>{document.querySelectorAll('[data-editor-tab]').forEach(item=>item.classList.toggle('active',item===tab));document.querySelectorAll('[data-editor-pane]').forEach(pane=>pane.classList.toggle('active',pane.dataset.editorPane===tab.dataset.editorTab));}));
document.querySelectorAll('[data-app-preview]').forEach(tab=>tab.addEventListener('click',()=>renderAppPreview(tab.dataset.appPreview)));
el('viewport-toggle').addEventListener('click',()=>{mobileViewport=!mobileViewport;el('viewport-toggle').textContent=mobileViewport?'Mobile':'Desktop';document.querySelector('.device').classList.toggle('mobile',mobileViewport);});
el('color-proof').addEventListener('click',()=>{grayscaleProof=!grayscaleProof;el('color-proof').textContent='Color proof: '+(grayscaleProof?'grayscale':'full');document.querySelector('.device').classList.toggle('grayscale',grayscaleProof);});
el('preview-cards').addEventListener('click',event=>{const button=event.target.closest('.sample-action');if(!button)return;const pressed=button.getAttribute('aria-pressed')==='true';button.setAttribute('aria-pressed',String(!pressed));button.textContent=pressed?'Preview control':'Control active ✓';});
el('undo').addEventListener('click',undoDraft);el('redo').addEventListener('click',redoDraft);
document.querySelector('.inspector').addEventListener('input',event=>{if(event.target.id!=='palette-harmony')scheduleHistory();});document.querySelector('.inspector').addEventListener('change',event=>{if(event.target.id!=='palette-harmony')scheduleHistory();});
document.addEventListener('keydown',event=>{if(!(event.ctrlKey||event.metaKey)||event.altKey||!['z','y'].includes(event.key.toLowerCase()))return;if(event.target.matches('input:not([type="color"]),textarea'))return;event.preventDefault();if(event.key.toLowerCase()==='y'||event.shiftKey)redoDraft();else undoDraft();});
el('theme-search').addEventListener('input',renderThemeList);
el('palette-reset').addEventListener('click',()=>{const selected=themes.find(theme=>theme.id===el('theme-picker').value);if(selected){const palette=editMode==='light'?(selected.document.variants?.light||selected.document.palette):selected.document.palette;variants[editMode]=palette;fillPalette(palette);scheduleHistory();message('Palette reset from '+selected.document.metadata.name+'.','ok');}});
function hexToHsl(hex){const raw=hex.replace('#',''),values=[0,2,4].map(index=>parseInt(raw.slice(index,index+2),16)/255);const [r,g,b]=values,max=Math.max(r,g,b),min=Math.min(r,g,b),delta=max-min;let h=0,s=0,l=(max+min)/2;if(delta){s=delta/(1-Math.abs(2*l-1));switch(max){case r:h=((g-b)/delta)%6;break;case g:h=(b-r)/delta+2;break;default:h=(r-g)/delta+4;}h*=60;if(h<0)h+=360;}return [h,s,l];}
function hslToHex(h,s,l){h=((h%360)+360)%360;const c=(1-Math.abs(2*l-1))*s,x=c*(1-Math.abs((h/60)%2-1)),m=l-c/2;let rgb=h<60?[c,x,0]:h<120?[x,c,0]:h<180?[0,c,x]:h<240?[0,x,c]:h<300?[x,0,c]:[c,0,x];return '#'+rgb.map(value=>Math.round((value+m)*255).toString(16).padStart(2,'0')).join('').toUpperCase();}
el('palette-generate').addEventListener('click',()=>{const seed=hexToHsl(el('color-acid_green').value),angles={neon:[0,315,265,175,35,350],analogous:[0,-35,-75,35,75,-5],triadic:[0,120,240,180,60,320],complementary:[0,165,220,180,25,140]}[el('palette-harmony').value];const roles=['acid_green','hot_pink','purple','cyan','orange','red'];roles.forEach((key,index)=>{const [h,s,l]=seed;const sat=index===0?s:Math.max(.58,Math.min(.92,s));const light=index===0?l:(key==='purple'?.62:key==='orange'?.54:.58);const hex=hslToHex(h+angles[index],sat,light);el('color-'+key).value=hex;el('hex-'+key).value=hex.slice(1);});preview();scheduleHistory();message('Accent colors generated from Acid Green. Review the contrast report before saving.','ok');});
el('duplicate-theme-2').addEventListener('click',duplicateTheme);el('delete-theme-2').addEventListener('click',deleteTheme);el('export-theme-2').addEventListener('click',exportTheme);el('import-theme-2').addEventListener('click',()=>el('file').click());
document.querySelectorAll('#name,#family,#description,#author,#font-ui,#font-mono,#font-display,#density,#corners,#motion').forEach(input=>input.addEventListener('input',preview));
document.querySelectorAll('#density,#corners,#motion').forEach(input=>input.addEventListener('change',preview));
el('appearance').addEventListener('click',changeAppearance);
el('variant').addEventListener('change',changeVariant);
async function applySharedTheme(id) {
try {
await request('/api/active/'+encodeURIComponent(id),{method:'POST'});
await applyTheme(id);
el('theme-picker').value=id;
el('top-theme-picker').value=id;
syncTopThemeMenu();
renderThemeList();
message('Applied '+(themes.find(theme=>theme.id===id)?.document.metadata.name || id)+'.','ok');
} catch(error) { message(error.message,'error'); }
}
el('theme-picker').addEventListener('change',event=>applySharedTheme(event.target.value));
el('top-theme-picker').addEventListener('change',event=>applySharedTheme(event.target.value));
el('top-theme-button').addEventListener('click',()=>{
const menu=el('top-theme-menu'), opening=menu.hidden;
menu.hidden=!opening; el('top-theme-button').setAttribute('aria-expanded',String(opening));
if(opening){
const rect=el('top-theme-button').getBoundingClientRect();
const margin=8;
menu.style.top=Math.min(rect.bottom+margin,window.innerHeight-margin-menu.offsetHeight)+'px';
menu.style.left=Math.max(margin,Math.min(rect.right-menu.offsetWidth,window.innerWidth-margin-menu.offsetWidth))+'px';
const selected=menu.querySelector('[aria-selected="true"]');
if(selected) selected.scrollIntoView({block:'center'});
}
});
document.addEventListener('pointerdown',event=>{if(!event.target.closest('#top-theme-picker-wrap')&&!event.target.closest('.theme-picker'))closeTopThemeMenu();});
document.addEventListener('keydown',event=>{if(event.key==='Escape'&&!el('top-theme-menu').hidden){closeTopThemeMenu();el('top-theme-button').focus();}});
el('load-theme').addEventListener('click',()=>{const selected=themes.find(theme=>theme.id===el('theme-picker').value);if(selected&&confirmDiscardDraft()){editDocument(selected.document,selected.builtin?null:selected.id);resetHistory();message('Editing '+selected.document.metadata.name+'.','ok');}});
el('new-theme').addEventListener('click',()=>{if(confirmDiscardDraft())newTheme();});el('duplicate-theme').addEventListener('click',duplicateTheme);el('save-theme').addEventListener('click',saveTheme);el('delete-theme').addEventListener('click',deleteTheme);
el('create-from-preview').addEventListener('click',createThemeFromPreview);
el('import-theme').addEventListener('click',()=>el('file').click());el('file').addEventListener('change',event=>importTheme(event.target.files[0]));el('export-theme').addEventListener('click',exportTheme);
el('export-pack').addEventListener('click',exportPack);el('import-pack').addEventListener('click',()=>el('pack-file').click());el('pack-file').addEventListener('change',event=>importPack(event.target.files[0]));
el('install-curated-pack').addEventListener('click',installCuratedPack);
Promise.all([refreshLibrary().then(()=>newTheme()),loadCuratedLibrary()]).catch(error=>message(error.message,'error'));
</script>
<script>
(() => {
const nav = document.querySelector('.control-nav');
const views = document.querySelectorAll('.control-view');
const workspace = document.querySelector('.workspace');
const title = document.getElementById('module-title');
const copy = document.getElementById('module-copy');
const moduleCount = document.getElementById('module-count');
const resourceGrid = document.getElementById('module-resources');
const validationResults = document.getElementById('validation-results');
const publishOutput = document.getElementById('publish-output');
const resourceSearch = document.getElementById('resource-search');
const resourceCategory = document.getElementById('resource-category');
const resourceTag = document.getElementById('resource-tag');
const resourcePagination = document.getElementById('resource-pagination');
const resourcePageLabel = document.getElementById('resource-page');
const resourcePrev = document.getElementById('resource-prev');
const resourceNext = document.getElementById('resource-next');
const genericModuleToolbar = document.getElementById('generic-module-toolbar');
const siteActions = document.getElementById('site-actions');
const siteSource = document.getElementById('site-source');
const siteDialog = document.getElementById('site-profile-dialog');
const moduleResources = { creative: ['asset'], components: ['component'], effects: ['effect'], vault: ['snippet', 'asset'], sites: [], validation: [], publish: [] };
let catalog = [], siteProfiles = [], siteProfilesSource = '', activeView = 'dashboard', resourcePage = 0;
const resourcePageSize = 12;
const labels = {
creative: ['Creative Forge', 'DarkskyForge logo, glyph, font, and asset authoring will load here.'],
components: ['Component Forge', 'Reusable Cybercore components will be previewed and governed here.'],
effects: ['Effect Lab', 'Motion profiles and Web Kit effect recipes will be tested here.'],
vault: ['Snippet + Asset Vault', 'Reusable code, screenshots, fonts, SVGs, and provenance will be managed here.'],
sites: ['Site Profiles', 'Select a repository profile and inspect its theme, resources, and fallback contract.'],
validation: ['Validation', 'Schema, accessibility, provenance, syntax, and compatibility gates will run here.'],
publish: ['Publish', 'Review versioned kit output, commits, Actions, and Pages deployments here.']
};
function show(view) {
activeView = view;
resourcePage = 0;
const dashboard = view === 'dashboard';
const themes = view === 'themes';
workspace.hidden = !themes;
document.body.classList.toggle('control-only', !themes);
document.body.classList.toggle('theme-editor-visible', themes);
document.getElementById('control-dashboard').hidden = !dashboard;
document.getElementById('control-placeholder').hidden = dashboard || themes;
if (!dashboard && !themes) {
title.textContent = labels[view]?.[0] || 'Control Plane module';
copy.textContent = labels[view]?.[1] || 'This workspace is registered and ready for its editor.';
genericModuleToolbar.hidden = activeView === 'sites';
siteActions.hidden = activeView !== 'sites';
renderModule();
validationResults.replaceChildren();
publishOutput.hidden = true;
}
function renderModule() {
const allowedKinds = moduleResources[activeView] || [];
const search = resourceSearch.value.trim().toLowerCase();
const category = resourceCategory.value;
const tag = resourceTag.value;
const resources = allowedKinds.length
? catalog.filter(resource => allowedKinds.includes(resource.kind))
.filter(resource => !category || resource.category === category)
.filter(resource => !tag || resource.tags?.includes(tag))
.filter(resource => !search || [resource.id, resource.title, resource.description, ...(resource.tags || [])].join(' ').toLowerCase().includes(search))
: [];
if (activeView === 'sites') {
moduleCount.textContent = `${siteProfiles.length} site profiles`;
siteSource.textContent = siteProfilesSource || 'LOCAL SITE PROFILES';
resourcePagination.hidden = true;
resourceGrid.replaceChildren(...siteProfiles.map(renderSite));
return;
}
const pageCount = Math.max(1, Math.ceil(resources.length / resourcePageSize));
resourcePage = Math.min(resourcePage, pageCount - 1);
const page = resources.slice(resourcePage * resourcePageSize, (resourcePage + 1) * resourcePageSize);
moduleCount.textContent = resources.length ? `${resources.length} governed resources` : 'Workspace operation';
resourcePagination.hidden = resources.length <= resourcePageSize;
resourcePageLabel.textContent = `${resourcePage + 1} / ${pageCount}`;
resourcePrev.disabled = resourcePage === 0;
resourceNext.disabled = resourcePage >= pageCount - 1;
resourceGrid.replaceChildren(...page.map(renderResource));
}
function refreshResourceFilters() {
const values = catalog.filter(resource => (moduleResources[activeView] || []).includes(resource.kind));
const categories = [...new Set(values.map(resource => resource.category).filter(Boolean))].sort();
const tags = [...new Set(values.flatMap(resource => resource.tags || []))].sort();
const update = (select, options, label) => {
const previous = select.value;
select.replaceChildren(new Option(label, ''), ...options.map(value => new Option(value, value)));
select.value = options.includes(previous) ? previous : '';
};
update(resourceCategory, categories, 'All categories');
update(resourceTag, tags, 'All tags');
}
nav.querySelectorAll('button').forEach(button => button.classList.toggle('active', button.dataset.controlView === view));
}
function renderResource(resource) {
const card = document.createElement('article');
card.className = 'resource-card';
const heading = document.createElement('h3');
heading.textContent = resource.title || resource.id;
const description = document.createElement('p');
description.textContent = resource.description || `${resource.id} · ${resource.status || 'unclassified'}`;
const meta = document.createElement('div');
meta.className = 'resource-meta';
[resource.category, ...(resource.tags || []), resource.kind, resource.version, resource.license, resource.language || resource.format].filter(Boolean).forEach(value => {
const badge = document.createElement('span');
badge.textContent = value;
meta.appendChild(badge);
});
card.append(heading, description, meta);
if (resource.content) {
const code = document.createElement('pre');
code.textContent = resource.content;
const button = document.createElement('button');
button.className = 'copy-resource';
button.type = 'button';
button.textContent = 'Copy resource';
button.addEventListener('click', async () => {
await navigator.clipboard.writeText(resource.content);
button.textContent = 'Copied';
setTimeout(() => { button.textContent = 'Copy resource'; }, 1200);
});
card.append(code, button);
}
return card;
}
function renderSite(site) {
const card = document.createElement('article');
card.className = 'resource-card site-card';
const head = document.createElement('div');
head.className = 'site-card-head';
const identity = document.createElement('div');
const glyph = document.createElement('span');
glyph.className = 'site-glyph';
glyph.textContent = (site.id || 'site').slice(0, 2).toUpperCase();
const heading = document.createElement('h3');
heading.textContent = site.id.replaceAll('-', ' ');
const repository = document.createElement('div');
repository.className = 'site-repository';
repository.textContent = site.repository;
identity.append(heading, repository);
const status = document.createElement('p');
const statusKind = site.status === 'MISSING RESOURCES' ? 'missing' : site.status === 'STALE' ? 'stale' : 'ready';
status.className = `site-status ${statusKind}`;
status.textContent = site.status || (site.stale ? 'STALE' : 'READY');
head.append(glyph, identity, status);
const details = document.createElement('div');
details.className = 'site-details';
[['Platform', site.platform], ['Active theme', site.theme || 'default'], ['Motion', site.motion], ['Fallback', site.fallback], ['Resources', site.resources?.length || 0], ['Last validation', site.last_validation || 'Not validated']].forEach(([label, value]) => {
const item = document.createElement('div');
item.className = 'site-detail';
const small = document.createElement('small');
small.textContent = label;
const strong = document.createElement('strong');
strong.textContent = value;
item.append(small, strong);
details.append(item);
});
const contract = document.createElement('div');
contract.className = 'site-contract';
Object.entries(site.resource_contract || {}).filter(([, count]) => count > 0).forEach(([kind, count]) => {
const badge = document.createElement('span');
badge.textContent = `${kind}: ${count}`;
contract.append(badge);
});
const issue = document.createElement('div');
issue.className = 'site-issue';
issue.textContent = site.missing_resources?.length ? `Missing: ${site.missing_resources.join(', ')}` : 'All attached resources resolved';
const actions = document.createElement('div');
actions.className = 'site-card-actions';
[['Inspect', () => { details.hidden = !details.hidden; }], ['Validate', () => document.getElementById('validate-control-plane').click()], ['Preview', () => { copy.textContent = `${site.id} preview · ${site.platform} · ${site.theme || 'default'}`; }], ['Compare', () => { publishOutput.textContent = JSON.stringify({ site: site.id, compare: 'local draft vs starter profile', resources: site.resources }, null, 2); publishOutput.hidden = false; }], ['Publish plan', () => document.getElementById('publish-control-plane').click()]].forEach(([label, action]) => {
const button = document.createElement('button');
button.type = 'button';
button.textContent = label;
button.addEventListener('click', action);
actions.append(button);
});
card.append(head, details, contract, issue, actions);
return card;
}
function renderTelemetry(data) {
const inventory = data.inventory || {};
const counts = inventory.kinds || {};
const statuses = inventory.statuses || {};
const sites = data.sites || {};
const siteStatuses = sites.statuses || {};
const set = (id, value) => { const node = document.getElementById(id); if (node) node.textContent = value; };
const rows = (id, entries) => {
const node = document.getElementById(id);
if (!node) return;
node.replaceChildren(...Object.entries(entries).map(([label, value]) => {
const row = document.createElement('span');
row.className = 'telemetry-row';
const key = document.createElement('span');
key.textContent = label.replaceAll('_', ' ');
const amount = document.createElement('strong');
amount.textContent = value;
row.append(key, amount);
return row;
}));
};
set('workspace-status', `${data.workspace?.manifest || 'UNKNOWN'} · ${data.workspace?.path || 'workspace path unavailable'}`);
set('dashboard-uptime', formatDuration(data.uptime_seconds));
set('dashboard-version', `v${data.cybercore_version || '—'}`);
set('dashboard-schema', `Control Plane ${data.control_plane_schema || '—'}`);
set('dashboard-theme-count', inventory.themes ?? '—');
set('dashboard-active-theme', `Active theme: ${inventory.active_theme || 'not selected'}`);
set('dashboard-resource-count', inventory.resources ?? '—');
set('dashboard-site-count', sites.total ?? '—');
set('dashboard-validation', data.validation?.passed ? 'PASS' : 'CHECK');
set('dashboard-validation-detail', data.validation?.last_run ? `Last run: ${formatTimestamp(data.validation.last_run.timestamp)}` : 'No run recorded');
set('dashboard-response', `${Math.round(data.response_ms || 0)} ms`);
set('dashboard-errors', `${data.errors?.api || 0} API errors · ${data.errors?.catalog || 0} catalog errors`);
rows('dashboard-runtime', { api_response: `${Math.round(data.response_ms || 0)} ms`, uptime: formatDuration(data.uptime_seconds), workspace: data.workspace?.valid ? 'VALID' : 'NOT CONFIGURED' });
rows('dashboard-schema', { schema: data.control_plane_schema || '—', manifest: data.workspace?.manifest || 'UNKNOWN' });
rows('dashboard-resource-breakdown', { components: counts.component || 0, effects: counts.effect || 0, snippets: counts.snippet || 0, assets: counts.asset || 0, fonts: counts.font || 0, svg: counts.svg || 0, packs: counts.pack || 0 });
rows('dashboard-site-breakdown', { ready: siteStatuses.READY || 0, stale: siteStatuses.STALE || 0, missing: siteStatuses['MISSING RESOURCES'] || 0 });
rows('dashboard-coverage', { license_provenance: `${data.coverage?.license_provenance_percent || 0}%`, accessibility_motion: `${data.coverage?.accessibility_reduced_motion_percent || 0}%`, approved: statuses.approved || 0, draft: statuses.draft || 0, review: statuses.review || 0 });
const health = document.getElementById('dashboard-site-health');
health.replaceChildren(...(sites.profiles || []).map(site => {
const item = document.createElement('div');
item.className = 'site-health';
const name = document.createElement('strong');
name.textContent = site.status || 'UNKNOWN';
const label = document.createElement('span');
label.textContent = site.id;
item.append(name, label);
return item;
}));
['cloudflare', 'google', 'github'].forEach(provider => {
const connector = data.connectors?.[provider] || {};
const state = connector.status || 'NOT CONFIGURED';
const node = document.getElementById(`connector-${provider}`);
node.textContent = state;
node.classList.toggle('connected', state === 'CONNECTED');
const metrics = connector.metrics || {};
const entries = Object.fromEntries(Object.entries(metrics).filter(([, value]) => ['string', 'number', 'boolean'].includes(typeof value) && value !== ''));
rows(`connector-metrics-${provider}`, Object.keys(entries).length ? entries : { detail: connector.error || 'No metrics available' });
});
const activity = document.getElementById('dashboard-activity');
activity.replaceChildren(...(data.activity || []).slice().reverse().map(item => {
const row = document.createElement('span');
row.className = 'telemetry-row';
row.innerHTML = `<span>${item.label || item.kind}</span><strong>${formatTimestamp(item.timestamp)}</strong>`;
return row;
}));
if (!data.activity?.length) {
const empty = document.createElement('span');
empty.className = 'telemetry-empty';
empty.textContent = 'No activity recorded yet.';
activity.append(empty);
}
const services = document.getElementById('dashboard-services');
services.replaceChildren(...(data.services || []).map(service => {
const row = document.createElement('span');
row.className = 'telemetry-row';
const label = document.createElement('span');
label.textContent = service.name;
const state = document.createElement('strong');
state.textContent = service.status;
row.append(label, state);
return row;
}));
const updates = document.getElementById('dashboard-updates');
updates.replaceChildren(...Object.entries(data.updates || {}).map(([name, update]) => {
const row = document.createElement('span');
row.className = 'telemetry-row';
const label = document.createElement('span');
label.textContent = name.replaceAll('_', ' ');
const state = document.createElement('strong');
state.className = 'neutral';
state.textContent = `${update.current} · ${update.status}`;
row.append(label, state);
return row;
}));
const warnings = document.getElementById('dashboard-warnings');
warnings.replaceChildren(...(data.warnings || []).map(warning => {
const row = document.createElement('span');
row.className = 'telemetry-row';
const label = document.createElement('span');
label.textContent = warning.message;
const state = document.createElement('strong');
state.className = warning.level === 'warning' ? 'fail' : 'neutral';
state.textContent = warning.level.toUpperCase();
row.append(label, state);
return row;
}));
if (!data.warnings?.length) {
const empty = document.createElement('span');
empty.className = 'telemetry-empty';
empty.textContent = 'No warnings.';
warnings.append(empty);
}
}
function formatDuration(seconds) {
if (!Number.isFinite(seconds)) return '—';
if (seconds < 60) return `${Math.round(seconds)}s`;
return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
}
function formatTimestamp(value) {
if (!value) return '—';
const date = new Date(Number(value) * 1000);
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
nav.addEventListener('click', event => {
const button = event.target.closest('[data-control-view]');
if (button) {
show(button.dataset.controlView);
refreshResourceFilters();
renderModule();
}
});
show('dashboard');
fetch('/api/control-plane/catalog').then(response => response.json()).then(data => {
catalog = data.catalog?.resources || [];
return fetch('/api/control-plane/workspace');
}).then(response => response.json()).then(data => {
const workspaceData = data.workspace || {};
const resources = workspaceData.resources || [];
const themes = resources.filter(resource => resource.kind === 'theme').length;
document.getElementById('workspace-status').textContent = data.loaded
? `${workspaceData.name} · ${data.path}`
: `${workspaceData.name} · no manifest loaded yet`;
document.getElementById('dashboard-theme-count').textContent = themes;
document.getElementById('dashboard-resource-count').textContent = resources.length;
document.getElementById('dashboard-site-count').textContent = (workspaceData.sites || []).length;
document.getElementById('workspace-resources').replaceChildren(...resources.slice(0, 8).map(resource => {
const row = document.createElement('span');
const resourceTitle = document.createElement('b');
const resourceKind = document.createElement('em');
resourceTitle.textContent = resource.title || resource.id;
resourceKind.textContent = resource.kind || 'resource';
row.replaceChildren(resourceTitle, resourceKind);
return row;
}));
return fetch('/api/control-plane/sites').then(response => response.ok ? response.json() : { sites: [] });
}).then(data => {
siteProfiles = data.sites || [];
siteProfilesSource = data.source || '';
document.getElementById('dashboard-site-count').textContent = siteProfiles.length;
moduleCount.textContent = `${siteProfiles.length} site profiles`;
refreshResourceFilters();
}).catch(() => { document.getElementById('workspace-status').textContent = 'Control Plane catalog unavailable'; });
[resourceSearch, resourceCategory, resourceTag].forEach(control => control.addEventListener('input', renderModule));
resourcePrev.addEventListener('click', () => { resourcePage -= 1; renderModule(); });
resourceNext.addEventListener('click', () => { resourcePage += 1; renderModule(); });
document.getElementById('validate-control-plane').addEventListener('click', async () => {
const response = await fetch('/api/control-plane/validate', { method: 'POST' });
const result = await response.json();
validationResults.replaceChildren(...(result.checks || []).map(check => {
const row = document.createElement('div');
const label = document.createElement('span');
label.textContent = check.label;
const status = document.createElement('strong');
status.className = check.passed ? 'pass' : 'fail';
status.textContent = check.passed ? 'PASS' : 'FAIL';
row.append(label, status);
return row;
}));
moduleCount.textContent = `${result.resource_count || 0} resources · ${result.valid ? 'ready' : 'needs attention'}`;
});
document.getElementById('publish-control-plane').addEventListener('click', async () => {
const response = await fetch('/api/control-plane/publish', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ version: 'v2', target: 'local-preview' })
});
publishOutput.textContent = JSON.stringify(await response.json(), null, 2);
publishOutput.hidden = false;
});
async function refreshTelemetry() {
const response = await fetch('/api/control-plane/telemetry');
if (!response.ok) throw new Error('Telemetry unavailable');
renderTelemetry(await response.json());
}
document.getElementById('refresh-telemetry').addEventListener('click', () => refreshTelemetry().catch(() => {
document.getElementById('workspace-status').textContent = 'Local telemetry unavailable';
}));
refreshTelemetry().catch(() => { document.getElementById('workspace-status').textContent = 'Local telemetry unavailable'; });
const siteSteps = [...document.querySelectorAll('[data-site-step]')];
let siteStep = 1;
function renderSiteStep() {
siteSteps.forEach(step => { step.hidden = Number(step.dataset.siteStep) !== siteStep; });
document.getElementById('site-step-back').hidden = siteStep === 1;
document.getElementById('site-step-next').hidden = siteStep === siteSteps.length;
document.getElementById('site-create-submit').hidden = siteStep !== siteSteps.length;
}
document.getElementById('create-site-profile').addEventListener('click', () => { siteStep = 1; renderSiteStep(); siteDialog.showModal(); });
document.getElementById('site-step-next').addEventListener('click', () => { siteStep = Math.min(siteSteps.length, siteStep + 1); renderSiteStep(); });
document.getElementById('site-step-back').addEventListener('click', () => { siteStep = Math.max(1, siteStep - 1); renderSiteStep(); });
document.getElementById('site-dialog-cancel').addEventListener('click', () => siteDialog.close());
document.getElementById('site-profile-form').addEventListener('submit', event => {
event.preventDefault();
const repository = document.getElementById('site-repository').value.trim();
const id = repository.split('/').filter(Boolean).pop()?.replace(/[^a-z0-9]+/gi, '-').toLowerCase() || 'new-site';
siteProfiles = [{
id, repository, platform: document.getElementById('site-platform').value,
theme: document.getElementById('site-theme').value.trim(), resources: document.getElementById('site-resources').value.split(',').map(value => value.trim()).filter(Boolean),
resource_contract: {}, motion: document.getElementById('site-motion').value, fallback: document.getElementById('site-fallback').value,
last_validation: 'Draft · not validated', missing_resources: [], status: 'READY', stale: false
}, ...siteProfiles];
siteProfilesSource = 'LOCAL DRAFT · NOT PUBLISHED';
siteDialog.close();
renderModule();
});
})();
</script>
</body>
</html>