<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>lytics โ€” cyberstates</title>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
        <link
            href="https://fonts.googleapis.com/css2?family=Play:wght@400;700&display=swap"
            rel="stylesheet"
        />
        <style>
            :root {
                --cyber-black: #000000;
                --cyber-dark: #0a0a0a;
                --cyber-green: #00ff41;
                --cyber-cyan: #00e5ff;
                --cyber-magenta: #ff00ff;
                --cyber-yellow: #ffd700;
                --cyber-orange: #ff6600;
                --cyber-red: #ff0040;
                --cyber-dim: #1a1a1a;
                --cyber-muted: #333333;
                --cyber-text: #e0e0e0;
            }
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
            html {
                overflow-y: scroll;
                scrollbar-gutter: stable;
            }
            body {
                font-family: "Play", sans-serif;
                background: var(--cyber-black);
                color: var(--cyber-text);
                min-height: 100vh;
                -webkit-font-smoothing: antialiased;
            }
            ::-webkit-scrollbar {
                width: 6px;
            }
            ::-webkit-scrollbar-track {
                background: var(--cyber-black);
            }
            ::-webkit-scrollbar-thumb {
                background: var(--cyber-muted);
                border-radius: 3px;
            }
            ::-webkit-scrollbar-thumb:hover {
                background: var(--cyber-green);
            }

            .page-frame {
                max-width: 1100px;
                margin: 0 auto;
                padding: 20px 24px 80px;
            }
            @media (max-width: 640px) {
                .page-frame {
                    padding: 12px 10px 72px;
                }
            }

            /* header */
            .header-row1 {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 16px;
                margin-bottom: 8px;
                flex-wrap: wrap;
            }
            .logo {
                font-size: clamp(22px, 4vw, 36px);
                font-weight: 700;
                line-height: 1.1;
                color: #fff;
                text-decoration: none;
            }
            .logo span.mark {
                color: var(--cyber-green);
            }
            .logo-suffix {
                min-height: 14px;
                line-height: 14px;
                font-size: 11px;
                letter-spacing: 3px;
                text-transform: uppercase;
                color: rgba(0, 255, 65, 0.55);
                margin-top: 4px;
            }
            .nav-row {
                display: flex;
                gap: 8px;
                flex-wrap: wrap;
                align-items: center;
            }
            .nav-btn {
                font-family: "Play", sans-serif;
                font-size: 12px;
                font-weight: 700;
                letter-spacing: 2px;
                color: #000;
                text-decoration: none;
                background: var(--btn-c);
                border: 1px solid var(--btn-c);
                padding: 8px 14px;
                border-radius: 2px;
                box-shadow: 0 0 14px
                    color-mix(in srgb, var(--btn-c) 35%, transparent);
                transition: all 0.15s;
                white-space: nowrap;
            }
            .nav-btn:hover {
                filter: brightness(1.1);
            }
            .nav-states {
                --btn-c: var(--cyber-green);
            }
            .nav-tokens {
                --btn-c: var(--cyber-yellow);
            }
            .nav-lytics {
                --btn-c: var(--cyber-cyan);
            }
            .nav-btn.nav-here {
                background: transparent;
                color: var(--btn-c);
                box-shadow: none;
            }

            .meta-line {
                font-size: 12px;
                letter-spacing: 1px;
                color: #555;
                margin: 8px 0 20px;
                line-height: 1.6;
            }
            .meta-line time {
                color: #777;
            }

            /* KPI cards โ€” match .stat-grid / .stat-card */
            .section-label {
                font-size: 11px;
                letter-spacing: 3px;
                text-transform: uppercase;
                color: #555;
                margin: 28px 0 12px;
            }
            .stat-grid {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
                gap: 12px;
            }
            .stat-card {
                background: #050505;
                border: 1px solid #1a1a1a;
                border-radius: 2px;
                padding: 14px 16px;
                min-width: 0;
            }
            .stat-label {
                font-size: 10px;
                letter-spacing: 2px;
                text-transform: uppercase;
                color: #555;
                margin-bottom: 8px;
            }
            .stat-value {
                font-size: clamp(18px, 3vw, 26px);
                font-weight: 700;
                color: var(--cyber-green);
                font-variant-numeric: tabular-nums;
                line-height: 1.15;
            }
            .stat-value.cyan {
                color: var(--cyber-cyan);
            }
            .stat-value.yellow {
                color: var(--cyber-yellow);
            }
            .stat-value.orange {
                color: var(--cyber-orange);
            }
            .stat-value.magenta {
                color: var(--cyber-magenta);
            }
            .stat-hint {
                font-size: 11px;
                color: #444;
                margin-top: 6px;
                letter-spacing: 0.5px;
            }

            .hint {
                font-size: 12px;
                color: #3a3a3a;
                margin: 14px 0 0;
                letter-spacing: 0.5px;
                line-height: 1.5;
            }

            /* window chips */
            .window-chips {
                display: flex;
                gap: 6px;
                flex-wrap: wrap;
                margin: 0 0 8px;
            }
            .chip {
                font-family: "Play", sans-serif;
                font-size: 11px;
                font-weight: 700;
                letter-spacing: 1.5px;
                text-transform: uppercase;
                padding: 6px 12px;
                border: 1px solid #222;
                border-radius: 2px;
                background: transparent;
                color: #666;
                cursor: pointer;
            }
            .chip:hover {
                border-color: var(--cyber-green);
                color: var(--cyber-green);
            }
            .chip.on {
                background: var(--cyber-green);
                border-color: var(--cyber-green);
                color: #000;
            }

            /* pulse โ€” dual metric compare */
            .pulse-wrap {
                border: 1px solid #111;
                border-radius: 2px;
                background: #050505;
                padding: 12px 12px 8px;
                position: relative;
            }
            .pulse-head {
                display: flex;
                align-items: center;
                gap: 8px;
                flex-wrap: wrap;
                margin-bottom: 10px;
            }
            .pulse-metric-btn {
                font-family: "Play", sans-serif;
                font-size: 11px;
                font-weight: 700;
                letter-spacing: 1.5px;
                text-transform: uppercase;
                padding: 6px 12px;
                border-radius: 2px;
                border: 1px solid;
                background: transparent;
                cursor: pointer;
                display: inline-flex;
                align-items: center;
                gap: 6px;
            }
            .pulse-metric-btn .swatch {
                width: 8px;
                height: 8px;
                border-radius: 1px;
                flex: 0 0 auto;
            }
            .pulse-metric-btn.a {
                color: var(--cyber-green);
                border-color: rgba(0, 255, 65, 0.45);
            }
            .pulse-metric-btn.a .swatch {
                background: var(--cyber-green);
            }
            .pulse-metric-btn.a:hover,
            .pulse-metric-btn.a.open {
                background: rgba(0, 255, 65, 0.1);
                border-color: var(--cyber-green);
            }
            .pulse-metric-btn.b {
                color: var(--cyber-magenta);
                border-color: rgba(255, 0, 255, 0.45);
            }
            .pulse-metric-btn.b .swatch {
                background: var(--cyber-magenta);
            }
            .pulse-metric-btn.b:hover,
            .pulse-metric-btn.b.open {
                background: rgba(255, 0, 255, 0.1);
                border-color: var(--cyber-magenta);
            }
            .pulse-vs {
                font-size: 10px;
                letter-spacing: 2px;
                color: #444;
            }
            .pulse-menu {
                position: absolute;
                z-index: 20;
                top: 48px;
                min-width: 180px;
                background: #0a0a0a;
                border: 1px solid #222;
                border-radius: 2px;
                box-shadow: 0 8px 28px rgba(0, 0, 0, 0.7);
                padding: 4px;
                display: none;
            }
            .pulse-menu.on {
                display: block;
            }
            .pulse-menu.slot-a {
                left: 12px;
            }
            .pulse-menu.slot-b {
                left: 160px;
            }
            .pulse-menu button {
                display: block;
                width: 100%;
                text-align: left;
                font-family: "Play", sans-serif;
                font-size: 11px;
                font-weight: 700;
                letter-spacing: 1.5px;
                text-transform: uppercase;
                padding: 8px 10px;
                border: none;
                border-radius: 2px;
                background: transparent;
                color: #888;
                cursor: pointer;
            }
            .pulse-menu button:hover {
                background: #111;
                color: #eee;
            }
            .pulse-menu button.on {
                color: var(--cyber-green);
                background: rgba(0, 255, 65, 0.08);
            }
            .pulse-menu.slot-b button.on {
                color: var(--cyber-magenta);
                background: rgba(255, 0, 255, 0.08);
            }
            .pulse-menu button:disabled {
                color: #333;
                cursor: default;
            }
            .pulse-chart {
                width: 100%;
                height: 160px;
                display: block;
            }
            .pulse-chart .grid {
                stroke: #151515;
                stroke-width: 1;
            }
            .pulse-chart .axis-lab {
                fill: #444;
                font-size: 9px;
                font-family: "Play", sans-serif;
                letter-spacing: 0.5px;
            }
            .pulse-chart .x-lab {
                fill: #555;
                font-size: 9px;
                font-family: "Play", sans-serif;
            }
            .pulse-chart .line-a {
                fill: none;
                stroke: var(--cyber-green);
                stroke-width: 2;
                stroke-linejoin: round;
                stroke-linecap: round;
            }
            .pulse-chart .line-b {
                fill: none;
                stroke: var(--cyber-magenta);
                stroke-width: 2;
                stroke-linejoin: round;
                stroke-linecap: round;
            }
            .pulse-chart .area-a {
                fill: rgba(0, 255, 65, 0.08);
                stroke: none;
            }
            .pulse-chart .area-b {
                fill: rgba(255, 0, 255, 0.07);
                stroke: none;
            }
            .pulse-chart .dot-a {
                fill: var(--cyber-green);
            }
            .pulse-chart .dot-b {
                fill: var(--cyber-magenta);
            }
            .pulse-chart .hover-line {
                stroke: #333;
                stroke-width: 1;
                stroke-dasharray: 2 3;
            }
            .pulse-tip {
                position: absolute;
                pointer-events: none;
                background: #0a0a0a;
                border: 1px solid #222;
                border-radius: 2px;
                padding: 8px 10px;
                font-size: 11px;
                letter-spacing: 0.5px;
                color: #ccc;
                display: none;
                z-index: 15;
                min-width: 120px;
                box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6);
            }
            .pulse-tip.on {
                display: block;
            }
            .pulse-tip .t {
                color: #666;
                margin-bottom: 4px;
                font-size: 10px;
            }
            .pulse-tip .row-a {
                color: var(--cyber-green);
            }
            .pulse-tip .row-b {
                color: var(--cyber-magenta);
            }
            .pulse-empty {
                color: #444;
                font-size: 13px;
                padding: 40px 24px;
                letter-spacing: 1px;
                text-align: center;
            }

            /* live strip */
            .live-head {
                display: flex;
                align-items: baseline;
                justify-content: space-between;
                gap: 12px;
                flex-wrap: wrap;
                margin-bottom: 10px;
            }
            .live-pulse-dot {
                display: inline-block;
                width: 8px;
                height: 8px;
                border-radius: 50%;
                background: var(--cyber-red);
                box-shadow: 0 0 10px var(--cyber-red);
                margin-right: 8px;
                animation: live-blink 1.4s ease-in-out infinite;
            }
            @keyframes live-blink {
                0%,
                100% {
                    opacity: 1;
                }
                50% {
                    opacity: 0.35;
                }
            }
            .live-stats {
                font-size: 12px;
                letter-spacing: 1px;
                color: #666;
            }
            .live-stats b {
                color: var(--cyber-green);
                font-weight: 700;
            }
            .live-strip {
                display: flex;
                gap: 8px;
                overflow-x: auto;
                padding-bottom: 4px;
                min-height: 72px;
            }
            .live-card {
                flex: 0 0 auto;
                min-width: 160px;
                max-width: 220px;
                background: #050505;
                border: 1px solid #1a1a1a;
                border-radius: 2px;
                padding: 10px 12px;
            }
            .live-card .ago {
                font-size: 10px;
                letter-spacing: 1px;
                color: var(--cyber-red);
                margin-bottom: 4px;
            }
            .live-card .path {
                font-size: 13px;
                color: #eee;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                margin-bottom: 4px;
            }
            .live-card .meta {
                font-size: 11px;
                color: #555;
                letter-spacing: 0.5px;
            }
            .live-empty {
                color: #444;
                font-size: 13px;
                padding: 20px 8px;
                letter-spacing: 1px;
            }

            /* frequency histogram */
            .freq-grid {
                display: grid;
                grid-template-columns: 1fr 1fr;
                gap: 16px;
            }
            @media (max-width: 800px) {
                .freq-grid {
                    grid-template-columns: 1fr;
                }
            }
            .freq-panel {
                border: 1px solid #111;
                border-radius: 2px;
                background: #050505;
                padding: 12px 14px;
            }
            .freq-panel .stat-label {
                margin-bottom: 10px;
            }
            .freq-kpis {
                display: flex;
                gap: 14px;
                flex-wrap: wrap;
                margin-bottom: 12px;
                font-size: 11px;
                letter-spacing: 1px;
                color: #555;
            }
            .freq-kpis b {
                color: var(--cyber-cyan);
                font-weight: 700;
            }
            .freq-row {
                display: grid;
                grid-template-columns: 48px 1fr 36px;
                gap: 8px;
                align-items: center;
                margin-bottom: 6px;
            }
            .freq-row .lab {
                font-size: 11px;
                color: #888;
                font-variant-numeric: tabular-nums;
            }
            .freq-row .bar-track {
                height: 12px;
                background: #0a0a0a;
                border: 1px solid #151515;
                border-radius: 1px;
                overflow: hidden;
            }
            .freq-row .bar {
                height: 100%;
                background: linear-gradient(
                    90deg,
                    rgba(0, 255, 65, 0.35),
                    var(--cyber-green)
                );
                min-width: 0;
            }
            .freq-row .n {
                font-size: 11px;
                color: #aaa;
                text-align: right;
                font-variant-numeric: tabular-nums;
            }

            /* path flow */
            .flow-list {
                border: 1px solid #111;
                border-radius: 2px;
                background: #050505;
                overflow: hidden;
            }
            .flow-row {
                display: grid;
                grid-template-columns: 1fr auto 1fr auto;
                gap: 10px;
                align-items: center;
                padding: 10px 14px;
                border-bottom: 1px solid #111;
            }
            .flow-row:last-child {
                border-bottom: none;
            }
            .flow-row:hover {
                background: #080808;
            }
            .flow-from,
            .flow-to {
                font-size: 13px;
                color: #ccc;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            .flow-from {
                text-align: right;
                color: var(--cyber-cyan);
            }
            .flow-to {
                color: var(--cyber-green);
            }
            .flow-arrow {
                color: #444;
                font-size: 12px;
                letter-spacing: 1px;
            }
            .flow-meta {
                font-size: 11px;
                color: #666;
                font-variant-numeric: tabular-nums;
                white-space: nowrap;
            }
            .flow-bar {
                grid-column: 1 / -1;
                height: 3px;
                background: #111;
                border-radius: 1px;
                overflow: hidden;
                margin-top: -4px;
            }
            .flow-bar > i {
                display: block;
                height: 100%;
                background: var(--cyber-magenta);
                opacity: 0.7;
            }
            .flow-empty {
                color: #444;
                font-size: 13px;
                padding: 24px;
                letter-spacing: 1px;
            }

            /* funnel builder */
            .funnel-builder {
                border: 1px solid #111;
                border-radius: 2px;
                background: #050505;
                padding: 14px 16px;
            }
            .funnel-steps {
                display: flex;
                flex-wrap: wrap;
                gap: 8px;
                align-items: center;
                margin-bottom: 12px;
                min-height: 36px;
            }
            .funnel-step-chip {
                display: inline-flex;
                align-items: center;
                gap: 6px;
                font-size: 12px;
                font-weight: 700;
                letter-spacing: 0.5px;
                padding: 6px 8px 6px 12px;
                border: 1px solid rgba(0, 255, 65, 0.35);
                border-radius: 2px;
                background: rgba(0, 255, 65, 0.06);
                color: var(--cyber-green);
                max-width: 220px;
            }
            .funnel-step-chip .ord {
                color: #555;
                font-size: 10px;
                min-width: 1.2em;
            }
            .funnel-step-chip .lab {
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            .funnel-step-chip button {
                border: none;
                background: transparent;
                color: #666;
                cursor: pointer;
                font-size: 14px;
                line-height: 1;
                padding: 0 2px;
                font-family: "Play", sans-serif;
            }
            .funnel-step-chip button:hover {
                color: var(--cyber-red);
            }
            .funnel-arrow {
                color: #333;
                font-size: 12px;
            }
            .funnel-add-row {
                display: flex;
                flex-wrap: wrap;
                gap: 8px;
                align-items: center;
                margin-bottom: 12px;
            }
            .funnel-add-row input {
                flex: 1 1 160px;
                min-width: 120px;
                font-family: "Play", sans-serif;
                font-size: 13px;
                padding: 8px 12px;
                border: 1px solid #222;
                border-radius: 2px;
                background: #0a0a0a;
                color: #eee;
            }
            .funnel-add-row input:focus {
                outline: none;
                border-color: var(--cyber-green);
            }
            .funnel-add-row button,
            .funnel-presets button {
                font-family: "Play", sans-serif;
                font-size: 11px;
                font-weight: 700;
                letter-spacing: 1.5px;
                text-transform: uppercase;
                padding: 8px 12px;
                border: 1px solid #222;
                border-radius: 2px;
                background: transparent;
                color: #888;
                cursor: pointer;
            }
            .funnel-add-row button:hover,
            .funnel-presets button:hover {
                border-color: var(--cyber-green);
                color: var(--cyber-green);
            }
            .funnel-add-row button.primary {
                background: var(--cyber-green);
                border-color: var(--cyber-green);
                color: #000;
            }
            .funnel-presets {
                display: flex;
                flex-wrap: wrap;
                gap: 6px;
                margin-bottom: 14px;
            }
            .funnel-presets .tag {
                font-size: 11px;
                letter-spacing: 0.5px;
                padding: 4px 10px;
                border: 1px solid #1a1a1a;
                border-radius: 2px;
                background: #0a0a0a;
                color: #777;
                cursor: pointer;
                max-width: 160px;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            .funnel-presets .tag:hover {
                border-color: var(--cyber-cyan);
                color: var(--cyber-cyan);
            }
            .funnel-presets .tag.in {
                border-color: rgba(0, 255, 65, 0.4);
                color: #555;
                cursor: default;
            }
            .funnel-chart {
                display: flex;
                flex-direction: column;
                gap: 8px;
            }
            .funnel-row {
                display: grid;
                grid-template-columns: minmax(80px, 140px) 1fr auto;
                gap: 10px;
                align-items: center;
            }
            .funnel-row .step-lab {
                font-size: 12px;
                color: #aaa;
                text-align: right;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            .funnel-row .bar-wrap {
                height: 28px;
                background: #0a0a0a;
                border: 1px solid #151515;
                border-radius: 2px;
                overflow: hidden;
                position: relative;
            }
            .funnel-row .bar {
                height: 100%;
                background: linear-gradient(
                    90deg,
                    rgba(0, 229, 255, 0.25),
                    var(--cyber-cyan)
                );
                min-width: 0;
                transition: width 0.25s ease;
            }
            .funnel-row .nums {
                font-size: 12px;
                font-variant-numeric: tabular-nums;
                color: #ccc;
                white-space: nowrap;
                min-width: 110px;
                text-align: right;
            }
            .funnel-row .nums .drop {
                color: #555;
                margin-left: 6px;
            }
            .funnel-row .nums .drop.bad {
                color: var(--cyber-orange);
            }
            .funnel-empty {
                color: #444;
                font-size: 13px;
                padding: 20px 8px;
                letter-spacing: 1px;
            }
            .funnel-summary {
                margin-top: 12px;
                font-size: 12px;
                letter-spacing: 1px;
                color: #666;
            }
            .funnel-summary b {
                color: var(--cyber-magenta);
                font-weight: 700;
            }
            @media (max-width: 640px) {
                .funnel-row {
                    grid-template-columns: 1fr;
                    gap: 4px;
                }
                .funnel-row .step-lab {
                    text-align: left;
                }
                .funnel-row .nums {
                    text-align: left;
                    min-width: 0;
                }
            }
            .split-2 {
                display: grid;
                grid-template-columns: 1fr 1fr;
                gap: 16px;
            }
            @media (max-width: 800px) {
                .split-2 {
                    grid-template-columns: 1fr;
                }
            }

            /* retention heat cells */
            .ret-cell {
                text-align: right;
                font-variant-numeric: tabular-nums;
                font-weight: 700;
                font-size: 12px;
                letter-spacing: 0.5px;
                border-radius: 2px;
                padding: 6px 8px !important;
                min-width: 52px;
            }
            .ret-cell .ret-n {
                display: block;
                font-size: 10px;
                font-weight: 400;
                color: #555;
                margin-top: 2px;
            }
            .ret-cell.zero {
                color: #333;
                font-weight: 400;
            }
            .ret-cell.w0 {
                color: var(--cyber-green);
            }

            /* tables โ€” match .cyber-table */
            .table-wrap {
                overflow-x: auto;
                margin-top: 4px;
                border: 1px solid #111;
                border-radius: 2px;
            }
            .cyber-table {
                width: 100%;
                border-collapse: separate;
                border-spacing: 0;
                font-size: 13px;
            }
            .cyber-table th {
                background: #050505;
                border-bottom: 1px solid var(--cyber-muted);
                padding: 12px 16px;
                text-align: left;
                font-size: 11px;
                font-weight: 700;
                letter-spacing: 2px;
                text-transform: uppercase;
                color: var(--cyber-green);
                white-space: nowrap;
                user-select: none;
            }
            .cyber-table th.num,
            .cyber-table td.num {
                text-align: right;
                font-variant-numeric: tabular-nums;
            }
            .cyber-table td {
                border-bottom: 1px solid #111;
                padding: 10px 16px;
                color: #ccc;
            }
            .cyber-table tbody tr:hover td {
                background: #080808;
                color: #eee;
            }
            .cyber-table tbody tr:last-child td {
                border-bottom: none;
            }
            .cyber-table a {
                color: var(--cyber-cyan);
                text-decoration: none;
            }
            .cyber-table a:hover {
                color: var(--cyber-green);
            }
            /* path segment tree */
            .path-cell {
                display: flex;
                align-items: center;
                gap: 8px;
                min-width: 0;
            }
            .path-tog {
                flex: 0 0 auto;
                width: 18px;
                height: 18px;
                padding: 0;
                border: 1px solid #333;
                border-radius: 2px;
                background: #0a0a0a;
                color: var(--cyber-green);
                font-size: 11px;
                font-family: "Play", sans-serif;
                line-height: 1;
                cursor: pointer;
                display: inline-flex;
                align-items: center;
                justify-content: center;
            }
            .path-tog:hover {
                border-color: var(--cyber-green);
                background: #111;
            }
            .path-tog.leaf {
                visibility: hidden;
                pointer-events: none;
            }
            .path-lab {
                min-width: 0;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            .path-lab.group {
                color: #fff;
                font-weight: 700;
            }
            .path-lab.child {
                color: #aaa;
            }
            .path-count {
                flex: 0 0 auto;
                font-size: 10px;
                color: #555;
                letter-spacing: 0.5px;
            }
            tr.path-child td {
                background: #050505;
            }
            tr.path-child td:first-child {
                padding-left: 28px;
            }
            tr.path-group {
                cursor: pointer;
            }
            tr.path-group:hover td {
                background: #0c0c0c;
            }
            .empty {
                color: #444;
                font-size: 13px;
                padding: 16px;
                letter-spacing: 1px;
            }

            .dock-credit {
                margin-top: 48px;
                font-size: 12px;
                color: #333;
                letter-spacing: 1px;
            }
            .dock-credit a {
                color: var(--cyber-green);
                text-decoration: none;
                opacity: 0.75;
            }
            .dock-credit a:hover {
                opacity: 1;
            }

            @media (max-width: 640px) {
                .stat-grid {
                    grid-template-columns: repeat(2, minmax(0, 1fr));
                }
                .stat-value {
                    font-size: 18px;
                }
                .cyber-table th,
                .cyber-table td {
                    padding: 8px 10px;
                    font-size: 12px;
                }
                .nav-btn {
                    padding: 6px 10px;
                    font-size: 11px;
                    letter-spacing: 1px;
                }
            }
        </style>
    </head>
    <body>
        <div class="page-frame">
            <div class="header-row1">
                <div>
                    <a class="logo" href="/lytics/"
                        ><span class="mark">๐Ÿ“ˆ</span> LYTICS</a
                    >
                    <div class="logo-suffix" id="logo-suffix">
                        cyberstates ยท last 7 days
                    </div>
                </div>
                <div class="nav-row">
                    <a class="nav-btn nav-states" href="/">STATES</a>
                    <a class="nav-btn nav-tokens" href="/tokens">TOKENS</a>
                    <a class="nav-btn nav-lytics nav-here" href="/lytics/"
                        >LYTICS</a
                    >
                </div>
            </div>

            <div class="window-chips" id="chips"></div>
            <div class="window-chips" id="aud-chips"></div>
            <div class="meta-line" id="meta">loading windowโ€ฆ</div>

            <div class="section-label">
                <span class="live-pulse-dot" aria-hidden="true"></span>live
            </div>
            <div class="live-head">
                <div class="live-stats" id="live-stats">loadingโ€ฆ</div>
            </div>
            <div class="live-strip" id="live"></div>
            <p class="hint">
                last 15 minutes ยท wall-clock presence ยท refreshes with the dash
            </p>

            <div class="section-label">overview</div>
            <div class="stat-grid" id="ov"></div>
            <div class="stat-grid" id="ov-aud" style="margin-top: 12px"></div>
            <div class="freq-panel" style="margin-top: 12px">
                <div class="stat-label">scroll reach</div>
                <div class="freq-kpis" id="scroll-kpis"></div>
                <div id="scroll-hist"></div>
            </div>
            <p class="hint">
                core: neurons โ†’ visits โ†’ views ยท derivatives: depth =
                views/visit ยท dwell = attention/visit ยท att/neuron = total
                looking per unique key ยท scroll = max % reached per neuronร—page
                (from attention sensor) ยท NEW = first-seen in window ยท RETURNING
                = seen before window
            </p>

            <div class="section-label">frequency</div>
            <div class="freq-grid">
                <div class="freq-panel">
                    <div class="stat-label">visits per neuron</div>
                    <div class="freq-kpis" id="freq-visits-kpis"></div>
                    <div id="freq-visits"></div>
                </div>
                <div class="freq-panel">
                    <div class="stat-label">days active per neuron</div>
                    <div class="freq-kpis" id="freq-days-kpis"></div>
                    <div id="freq-days"></div>
                </div>
            </div>
            <div class="table-wrap" style="margin-top: 12px">
                <table class="cyber-table" id="freq-top"></table>
            </div>
            <p class="hint">
                shape of engagement ยท flat left = healthy long tail ยท spike on
                32+ = whales / bots ยท top list is heaviest keys (truncated)
            </p>

            <div class="section-label">retention</div>
            <div class="stat-grid" id="ret-kpis"></div>
            <div class="table-wrap" style="margin-top: 12px">
                <table class="cyber-table" id="retention"></table>
            </div>
            <p class="hint">
                cohort = week of first-seen ยท W0 = that week ยท Wn = still active
                n weeks later ยท % of cohort size ยท returns = first seen in
                window, came back within 7d
            </p>

            <div class="section-label">pulse</div>
            <div class="pulse-wrap" id="pulse-wrap">
                <div class="pulse-head">
                    <button
                        type="button"
                        class="pulse-metric-btn a"
                        id="pulse-ma"
                        aria-haspopup="listbox"
                    >
                        <span class="swatch"></span
                        ><span class="lab">NEURONS</span>
                    </button>
                    <span class="pulse-vs">vs</span>
                    <button
                        type="button"
                        class="pulse-metric-btn b"
                        id="pulse-mb"
                        aria-haspopup="listbox"
                    >
                        <span class="swatch"></span
                        ><span class="lab">ATTENTION</span>
                    </button>
                </div>
                <div class="pulse-menu" id="pulse-menu" role="listbox"></div>
                <div id="pulse"></div>
                <div class="pulse-tip" id="pulse-tip"></div>
            </div>
            <p class="hint">
                pick any two metrics ยท left axis green ยท right axis magenta ยท
                each scaled independently ยท day buckets (hour in 24h)
            </p>

            <div class="section-label">landings ยท exits</div>
            <div class="split-2">
                <div>
                    <div class="stat-label" style="margin-bottom: 8px">
                        entry pages
                    </div>
                    <div class="table-wrap">
                        <table class="cyber-table" id="entries"></table>
                    </div>
                </div>
                <div>
                    <div class="stat-label" style="margin-bottom: 8px">
                        exit pages
                    </div>
                    <div class="table-wrap">
                        <table class="cyber-table" id="exits"></table>
                    </div>
                </div>
            </div>
            <p class="hint">
                neurons = unique visitors who entered / left here ยท visits =
                passage count ยท grouped by first path segment (click to expand)
            </p>

            <div class="section-label">path flow</div>
            <div class="flow-list" id="pathflow"></div>
            <p class="hint">
                consecutive pageviews inside a visit ยท from โ†’ to ยท thickness =
                neurons who took the edge ยท answers "after X, where?"
            </p>

            <div class="section-label">funnel</div>
            <div class="funnel-builder">
                <div class="funnel-steps" id="funnel-steps"></div>
                <div class="funnel-add-row">
                    <input
                        id="funnel-input"
                        type="text"
                        placeholder="/path or paste pathname"
                        autocomplete="off"
                        spellcheck="false"
                    />
                    <button type="button" class="primary" id="funnel-add">
                        add step
                    </button>
                    <button type="button" id="funnel-clear">clear</button>
                </div>
                <div class="funnel-presets" id="funnel-presets"></div>
                <div class="funnel-chart" id="funnel-chart"></div>
                <div class="funnel-summary" id="funnel-summary"></div>
            </div>
            <p class="hint">
                ordered path steps ยท neuron must hit each step later than the
                previous (any visit, full window) ยท v1 = page paths only ยท
                convert/tx goals come next
            </p>

            <div class="section-label">top pages</div>
            <div class="table-wrap">
                <table class="cyber-table" id="particles"></table>
            </div>
            <p class="hint">
                grouped by first path segment ยท click a folder to expand
                children ยท segment neurons/visits are sums of child paths (may
                overlap)
            </p>

            <div class="section-label">geo</div>
            <div class="table-wrap">
                <table class="cyber-table" id="countries"></table>
            </div>

            <div class="section-label">devices</div>
            <div class="table-wrap">
                <table class="cyber-table" id="devices"></table>
            </div>

            <div class="section-label">visits by source</div>
            <div class="table-wrap">
                <table class="cyber-table" id="sources"></table>
            </div>

            <div class="section-label">visits by channel</div>
            <div class="table-wrap">
                <table class="cyber-table" id="channels"></table>
            </div>

            <div class="dock-credit">
                ๐Ÿด a <a href="https://cyberia.to">cyberia</a> project ยท
                <a href="https://cyberstates.net">cyberstates.net</a>
            </div>
        </div>

        <script>
            const root = location.pathname.replace(/\/?$/, "/");
            const WINDOWS = [
                {
                    id: "24h",
                    label: "24h",
                    ms: 24 * 3600 * 1000,
                    bucket: "hour",
                },
                {
                    id: "7d",
                    label: "7d",
                    ms: 7 * 24 * 3600 * 1000,
                    bucket: "day",
                },
                {
                    id: "30d",
                    label: "30d",
                    ms: 30 * 24 * 3600 * 1000,
                    bucket: "day",
                },
            ];
            const AUDIENCES = [
                { id: "all", label: "ALL" },
                { id: "new", label: "NEW" },
                { id: "returning", label: "RETURNING" },
            ];
            let windowId =
                new URLSearchParams(location.search).get("w") || "7d";
            if (!WINDOWS.some((w) => w.id === windowId)) windowId = "7d";
            let audienceId =
                new URLSearchParams(location.search).get("a") || "all";
            if (!AUDIENCES.some((a) => a.id === audienceId)) audienceId = "all";

            function windowSpec() {
                return WINDOWS.find((w) => w.id === windowId) || WINDOWS[1];
            }
            function audienceSpec() {
                return (
                    AUDIENCES.find((a) => a.id === audienceId) || AUDIENCES[0]
                );
            }
            function qs() {
                const w = windowSpec();
                const to = Date.now() + 1;
                const from = Date.now() - w.ms;
                return `?from=${from}&to=${to}&limit=50&audience=${audienceId}`;
            }
            const get = (name, extra = "") => {
                const base = qs();
                const join = extra.startsWith("?")
                    ? "&" + extra.slice(1)
                    : extra;
                return fetch(`${root}api/report/${name}${base}${join}`).then(
                    (r) => r.json(),
                );
            };

            function setQueryState() {
                const u = new URL(location.href);
                u.searchParams.set("w", windowId);
                u.searchParams.set("a", audienceId);
                history.replaceState(null, "", u);
            }

            const fmt = (ms) => {
                if (ms == null || ms === 0) return "0s";
                if (ms >= 60000) return `${(ms / 60000).toFixed(1)}m`;
                if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
                return `${ms}ms`;
            };
            const fmtDepth = (milli) =>
                milli == null ? "โ€”" : (milli / 1000).toFixed(2);
            const fmtDay = (ms) => {
                const d = new Date(ms);
                return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
            };
            const fmtHour = (ms) => {
                const d = new Date(ms);
                return `${String(d.getUTCHours()).padStart(2, "0")}h`;
            };
            const fmtStamp = (ms) =>
                new Date(ms)
                    .toISOString()
                    .replace("T", " ")
                    .replace(/\.\d{3}Z$/, " UTC");
            const shortPath = (p) => {
                if (!p) return "โ€”";
                if (p.length <= 42) return p;
                return p.slice(0, 20) + "โ€ฆ" + p.slice(-18);
            };

            function table(el, headers, data, cols) {
                if (!data || !data.length) {
                    el.innerHTML = `<tbody><tr><td class="empty" colspan="${headers.length}">โ€” no data in window</td></tr></tbody>`;
                    return;
                }
                el.innerHTML =
                    `<thead><tr>${headers
                        .map(
                            (h, i) =>
                                `<th class="${cols[i][1] || ""}">${h}</th>`,
                        )
                        .join("")}</tr></thead><tbody>` +
                    data
                        .map(
                            (r) =>
                                `<tr>${cols
                                    .map(
                                        ([c, cls]) =>
                                            `<td class="${cls || ""}">${r[c] ?? ""}</td>`,
                                    )
                                    .join("")}</tr>`,
                        )
                        .join("") +
                    `</tbody>`;
            }

            // path segment rollup: first segment = folder, rest = children.
            // expanded state survives window chip changes within the session.
            const pathExpanded = {
                entries: new Set(),
                exits: new Set(),
                particles: new Set(),
            };

            function pathParts(pathname) {
                if (!pathname || pathname === "/")
                    return { group: "/", rest: null };
                const segs = pathname
                    .replace(/^\//, "")
                    .split("/")
                    .filter(Boolean);
                if (segs.length === 0) return { group: "/", rest: null };
                if (segs.length === 1)
                    return { group: "/" + segs[0], rest: null };
                return {
                    group: "/" + segs[0],
                    rest: segs.slice(1).join("/"),
                };
            }

            /** @param rows raw API rows with .pathname
             *  @param sumKeys numeric keys to sum onto groups
             */
            function groupByPathSegment(rows, sumKeys) {
                const map = new Map();
                for (const row of rows || []) {
                    const path = row.pathname || "/";
                    const { group, rest } = pathParts(path);
                    let g = map.get(group);
                    if (!g) {
                        g = {
                            group,
                            metrics: Object.fromEntries(
                                sumKeys.map((k) => [k, 0]),
                            ),
                            children: [],
                            leaf: null, // single-segment path is the group itself
                        };
                        map.set(group, g);
                    }
                    for (const k of sumKeys) {
                        g.metrics[k] =
                            (g.metrics[k] || 0) + (Number(row[k]) || 0);
                    }
                    if (rest == null) {
                        // exact group path (e.g. /tokens or /)
                        g.leaf = row;
                    } else {
                        g.children.push({
                            label: rest,
                            pathname: path,
                            row,
                        });
                    }
                }
                // sort groups by first sum key desc (neurons preferred)
                const primary = sumKeys[0];
                const groups = [...map.values()].sort(
                    (a, b) =>
                        (b.metrics[primary] || 0) - (a.metrics[primary] || 0),
                );
                for (const g of groups) {
                    g.children.sort(
                        (a, b) =>
                            (Number(b.row[primary]) || 0) -
                            (Number(a.row[primary]) || 0),
                    );
                }
                return groups;
            }

            /**
             * render expandable path-segment table.
             * formatCells(row|metrics, isGroup) โ†’ array of cell HTML for metric cols
             * pathCol is always first.
             */
            function renderPathTable(
                el,
                expandKey,
                headers,
                groups,
                formatCells,
            ) {
                if (!groups || !groups.length) {
                    el.innerHTML = `<tbody><tr><td class="empty" colspan="${headers.length}">โ€” no data in window</td></tr></tbody>`;
                    return;
                }
                const expanded = pathExpanded[expandKey];
                const head = `<thead><tr>${headers
                    .map(
                        (h, i) =>
                            `<th class="${i === 0 ? "" : "num"}">${h}</th>`,
                    )
                    .join("")}</tr></thead>`;

                const pathCell = (
                    label,
                    { isGroup, open, nChildren, title },
                ) => {
                    const tog = isGroup
                        ? `<button type="button" class="path-tog" aria-expanded="${open}" data-group="${label}" title="${open ? "collapse" : "expand"}">${open ? "โˆ’" : "+"}</button>`
                        : `<span class="path-tog leaf" aria-hidden="true"></span>`;
                    const count =
                        isGroup && nChildren
                            ? `<span class="path-count">${nChildren}</span>`
                            : "";
                    return `<div class="path-cell">${tog}? "group" : "child"}" title="${title || label}">${label}</span>${count}</div>`;
                };

                let body = "";
                for (const g of groups) {
                    const hasKids = g.children.length > 0;
                    const open = hasKids && expanded.has(g.group);
                    // group with only a leaf and no kids โ†’ plain row
                    const isFolder = hasKids;
                    const cells = formatCells(g.metrics, true, g);
                    body += `<tr class="${isFolder ? "path-group" : ""}" ${isFolder ? `data-group="${g.group}"` : ""}>
            <td>${pathCell(g.group, {
                isGroup: isFolder,
                open,
                nChildren: g.children.length,
                title: g.group,
            })}</td>
            ${cells.map((c) => `${c}</td>`).join("")}
          </tr>`;
                    if (open) {
                        for (const ch of g.children) {
                            const chCells = formatCells(ch.row, false, g);
                            body += `<tr class="path-child">
              <td>${pathCell(ch.label, {
                  isGroup: false,
                  open: false,
                  nChildren: 0,
                  title: ch.pathname,
              })}</td>
              ${chCells.map((c) => `${c}</td>`).join("")}
            </tr>`;
                        }
                    }
                }
                el.innerHTML = head + `<tbody>${body}</tbody>`;

                // toggle handlers
                el.querySelectorAll("tr.path-group").forEach((tr) => {
                    const toggle = () => {
                        const key = tr.dataset.group;
                        if (expanded.has(key)) expanded.delete(key);
                        else expanded.add(key);
                        renderPathTable(
                            el,
                            expandKey,
                            headers,
                            groups,
                            formatCells,
                        );
                    };
                    tr.addEventListener("click", (e) => {
                        if (e.target.closest("a")) return;
                        toggle();
                    });
                });
            }

            function cards(items) {
                return items
                    .map(
                        ([label, value, hint, klass]) =>
                            `<div class="stat-card">
          <div class="stat-label">${label}</div>
          <div class="stat-value ${klass || ""}">${value ?? "โ€”"}</div>
          <div class="stat-hint">${hint}</div>
        </div>`,
                    )
                    .join("");
            }

            function renderChips() {
                document.getElementById("chips").innerHTML = WINDOWS.map(
                    (w) =>
                        `<button type="button" class="chip ${w.id === windowId ? "on" : ""}" data-w="${w.id}">${w.label}</button>`,
                ).join("");
                document.querySelectorAll("#chips .chip").forEach((btn) => {
                    btn.onclick = () => {
                        windowId = btn.dataset.w;
                        setQueryState();
                        refresh();
                    };
                });
                document.getElementById("aud-chips").innerHTML = AUDIENCES.map(
                    (a) =>
                        `<button type="button" class="chip ${a.id === audienceId ? "on" : ""}" data-a="${a.id}">${a.label}</button>`,
                ).join("");
                document.querySelectorAll("#aud-chips .chip").forEach((btn) => {
                    btn.onclick = () => {
                        audienceId = btn.dataset.a;
                        setQueryState();
                        refresh();
                    };
                });
            }

            function renderAudienceSplit(o) {
                const el = document.getElementById("ov-aud");
                // only when looking at ALL โ€” filtered views already are the slice
                if (
                    audienceId !== "all" ||
                    !o.new ||
                    !o.returning ||
                    typeof o.new !== "object"
                ) {
                    el.innerHTML = "";
                    return;
                }
                const n = o.new;
                const r = o.returning;
                const totalN = o.neurons || 0 || 1;
                const pct = (x) =>
                    `${Math.round(((Number(x) || 0) / totalN) * 100)}%`;
                el.innerHTML = cards([
                    [
                        "NEW NEURONS",
                        n.neurons ?? 0,
                        `${pct(n.neurons)} of window ยท first-seen here`,
                        "yellow",
                    ],
                    [
                        "NEW VISITS",
                        n.visits ?? 0,
                        `depth ${fmtDepth(n.views_per_visit_milli)} ยท dwell ${fmt(n.attention_ms_per_visit)}`,
                        "yellow",
                    ],
                    [
                        "NEW ATT",
                        fmt(n.attention_ms),
                        `att/neuron ${fmt(n.attention_ms_per_neuron)}`,
                        "yellow",
                    ],
                    [
                        "RET NEURONS",
                        r.neurons ?? 0,
                        `${pct(r.neurons)} of window ยท seen before`,
                        "cyan",
                    ],
                    [
                        "RET VISITS",
                        r.visits ?? 0,
                        `depth ${fmtDepth(r.views_per_visit_milli)} ยท dwell ${fmt(r.attention_ms_per_visit)}`,
                        "cyan",
                    ],
                    [
                        "RET ATT",
                        fmt(r.attention_ms),
                        `att/neuron ${fmt(r.attention_ms_per_neuron)}`,
                        "cyan",
                    ],
                ]);
            }

            const WEEK_MS = 7 * 24 * 3600 * 1000;

            function heatStyle(pct) {
                // 0..100 โ†’ green fill intensity on black
                if (pct == null || pct <= 0) return "";
                const a = Math.min(0.85, 0.08 + (pct / 100) * 0.75);
                return `background: rgba(0, 255, 65, ${a.toFixed(3)}); color: ${pct >= 40 ? "#000" : "var(--cyber-green)"}`;
            }

            function renderRetentionMatrix(rows) {
                const el = document.getElementById("retention");
                if (!rows || !rows.length) {
                    el.innerHTML = `<tbody><tr><td class="empty">โ€” not enough history for cohorts yet</td></tr></tbody>`;
                    return;
                }
                const maxW = Math.max(
                    ...rows.map((r) => (r.weeks || []).length),
                    1,
                );
                // oldest cohort on top, newest at bottom (classic)
                const sorted = [...rows].sort(
                    (a, b) =>
                        (a.cohort_start_ms || 0) - (b.cohort_start_ms || 0),
                );
                const now = Date.now();

                const head =
                    `<thead><tr><th>COHORT</th><th class="num">SIZE</th>` +
                    Array.from(
                        { length: maxW },
                        (_, i) => `<th class="num">W${i}</th>`,
                    ).join("") +
                    `</tr></thead>`;

                const body = sorted
                    .map((r) => {
                        const size = r.size || 0;
                        const weeks = r.weeks || [];
                        const start = r.cohort_start_ms || 0;
                        const cells = Array.from({ length: maxW }, (_, i) => {
                            // week offset not yet elapsed for this cohort
                            if (start + i * WEEK_MS > now) {
                                return `<td class="ret-cell zero num">โ€”</td>`;
                            }
                            if (i === 0) {
                                return `<td class="ret-cell w0 num" title="cohort week ยท size">${size}<span class="ret-n">100%</span></td>`;
                            }
                            if (!size) {
                                return `<td class="ret-cell zero num">โ€”</td>`;
                            }
                            const n = weeks[i] || 0;
                            const pct = Math.round((n / size) * 100);
                            if (n === 0) {
                                return `<td class="ret-cell zero num">0%<span class="ret-n">0</span></td>`;
                            }
                            return `<td class="ret-cell num" style="${heatStyle(pct)}" title="${n} of ${size} neurons still active">${pct}%<span class="ret-n">${n}</span></td>`;
                        }).join("");
                        return `<tr>
              <td>${fmtDay(start)}</td>
              <td class="num">${size}</td>
              ${cells}
            </tr>`;
                    })
                    .join("");

                el.innerHTML = head + `<tbody>${body}</tbody>`;
            }

            async function renderRetention(w) {
                // matrix: full event history (server ignores window)
                const matrix = await get("retention", "?weeks=8");
                renderRetentionMatrix(Array.isArray(matrix) ? matrix : []);

                // returns: first-seen in current window, came back within 7d
                const ret = await get("returns", `?horizon_ms=${WEEK_MS}`);
                const cohort = ret.cohort ?? 0;
                const returned = ret.returned ?? 0;
                const rate =
                    cohort > 0
                        ? `${Math.round((returned / cohort) * 100)}%`
                        : "โ€”";
                document.getElementById("ret-kpis").innerHTML = cards([
                    [
                        "NEW (WINDOW)",
                        cohort,
                        `first-seen in ${w.label}`,
                        "yellow",
                    ],
                    [
                        "RETURNED 7D",
                        returned,
                        "came back within 7 days",
                        "cyan",
                    ],
                    ["RETURN RATE", rate, "returned / new", "magenta"],
                ]);
            }

            // dual-metric pulse: any pair from the overview vocabulary
            const PULSE_METRICS = [
                {
                    id: "neurons",
                    label: "NEURONS",
                    hint: "unique keys",
                    value: (r) => Number(r.neurons) || 0,
                    fmt: (v) => String(Math.round(v)),
                },
                {
                    id: "visits",
                    label: "VISITS",
                    hint: "arrivals in bucket",
                    value: (r) => Number(r.visits) || 0,
                    fmt: (v) => String(Math.round(v)),
                },
                {
                    id: "views",
                    label: "VIEWS",
                    hint: "pageviews",
                    value: (r) => Number(r.views) || 0,
                    fmt: (v) => String(Math.round(v)),
                },
                {
                    id: "attention",
                    label: "ATTENTION",
                    hint: "minutes looking",
                    value: (r) => (Number(r.attention_ms) || 0) / 60000,
                    fmt: (v) =>
                        v >= 60
                            ? `${(v / 60).toFixed(1)}h`
                            : v >= 1
                              ? `${v.toFixed(1)}m`
                              : `${Math.round(v * 60)}s`,
                },
                {
                    id: "depth",
                    label: "DEPTH",
                    hint: "views / visit",
                    value: (r) => (Number(r.views_per_visit_milli) || 0) / 1000,
                    fmt: (v) => v.toFixed(2),
                },
                {
                    id: "dwell",
                    label: "DWELL",
                    hint: "attention / visit",
                    value: (r) => Number(r.attention_ms_per_visit) || 0,
                    fmt: (v) => fmt(v),
                },
                {
                    id: "att_neuron",
                    label: "ATT/NEURON",
                    hint: "looking per key",
                    value: (r) => Number(r.attention_ms_per_neuron) || 0,
                    fmt: (v) => fmt(v),
                },
            ];

            let pulseMetricA = localStorage.getItem("pulse_ma") || "neurons";
            let pulseMetricB = localStorage.getItem("pulse_mb") || "attention";
            let pulseMenuSlot = null; // 'a' | 'b' | null
            let pulseRows = [];
            let pulseBucket = "day";

            function metricById(id) {
                return (
                    PULSE_METRICS.find((m) => m.id === id) || PULSE_METRICS[0]
                );
            }

            function syncPulseButtons() {
                const a = metricById(pulseMetricA);
                const b = metricById(pulseMetricB);
                document.querySelector("#pulse-ma .lab").textContent = a.label;
                document.querySelector("#pulse-mb .lab").textContent = b.label;
                document
                    .getElementById("pulse-ma")
                    .classList.toggle("open", pulseMenuSlot === "a");
                document
                    .getElementById("pulse-mb")
                    .classList.toggle("open", pulseMenuSlot === "b");
            }

            function closePulseMenu() {
                pulseMenuSlot = null;
                const menu = document.getElementById("pulse-menu");
                menu.classList.remove("on", "slot-a", "slot-b");
                syncPulseButtons();
            }

            function openPulseMenu(slot) {
                pulseMenuSlot = slot;
                const menu = document.getElementById("pulse-menu");
                const current = slot === "a" ? pulseMetricA : pulseMetricB;
                const other = slot === "a" ? pulseMetricB : pulseMetricA;
                menu.innerHTML = PULSE_METRICS.map(
                    (m) =>
                        `<button type="button" role="option" data-id="${m.id}" class="${m.id === current ? "on" : ""}" ${m.id === other ? "disabled" : ""} title="${m.hint}">${m.label}${m.hint}</span></button>`,
                ).join("");
                menu.classList.add("on", slot === "a" ? "slot-a" : "slot-b");
                menu.classList.remove(slot === "a" ? "slot-b" : "slot-a");
                menu.querySelectorAll("button[data-id]").forEach((btn) => {
                    if (btn.disabled) return;
                    btn.onclick = (e) => {
                        e.stopPropagation();
                        const id = btn.dataset.id;
                        if (slot === "a") {
                            pulseMetricA = id;
                            localStorage.setItem("pulse_ma", id);
                        } else {
                            pulseMetricB = id;
                            localStorage.setItem("pulse_mb", id);
                        }
                        closePulseMenu();
                        renderPulse(pulseRows, pulseBucket);
                    };
                });
                syncPulseButtons();
            }

            function initPulsePickers() {
                const ma = document.getElementById("pulse-ma");
                const mb = document.getElementById("pulse-mb");
                if (ma.dataset.bound) return;
                ma.dataset.bound = "1";
                ma.onclick = (e) => {
                    e.stopPropagation();
                    if (pulseMenuSlot === "a") closePulseMenu();
                    else openPulseMenu("a");
                };
                mb.onclick = (e) => {
                    e.stopPropagation();
                    if (pulseMenuSlot === "b") closePulseMenu();
                    else openPulseMenu("b");
                };
                document.addEventListener("click", (e) => {
                    if (!e.target.closest("#pulse-wrap")) closePulseMenu();
                });
            }

            function renderPulse(rows, bucket) {
                pulseRows = Array.isArray(rows) ? rows : [];
                pulseBucket = bucket;
                initPulsePickers();
                syncPulseButtons();
                closePulseMenu();

                const el = document.getElementById("pulse");
                const tip = document.getElementById("pulse-tip");
                tip.classList.remove("on");

                if (!pulseRows.length) {
                    el.innerHTML = `<div class="pulse-empty">โ€” no pulse in window</div>`;
                    return;
                }

                const ma = metricById(pulseMetricA);
                const mb = metricById(pulseMetricB);
                const valsA = pulseRows.map((r) => ma.value(r));
                const valsB = pulseRows.map((r) => mb.value(r));
                const maxA = Math.max(1e-9, ...valsA);
                const maxB = Math.max(1e-9, ...valsB);

                const wrapEl = document.getElementById("pulse-wrap");
                const W = Math.max(320, (wrapEl.clientWidth || 640) - 24);
                const H = 160;
                const pad = { t: 12, r: 48, b: 28, l: 48 };
                const iw = W - pad.l - pad.r;
                const ih = H - pad.t - pad.b;
                const n = pulseRows.length;
                const xAt = (i) =>
                    pad.l + (n === 1 ? iw / 2 : (i / (n - 1)) * iw);
                const yA = (v) => pad.t + ih - (v / maxA) * ih;
                const yB = (v) => pad.t + ih - (v / maxB) * ih;

                const ptsA = valsA.map((v, i) => [xAt(i), yA(v)]);
                const ptsB = valsB.map((v, i) => [xAt(i), yB(v)]);
                const line = (pts) =>
                    pts
                        .map(
                            ([x, y], i) =>
                                `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`,
                        )
                        .join(" ");
                const area = (pts) => {
                    if (!pts.length) return "";
                    const base = pad.t + ih;
                    return (
                        line(pts) +
                        ` L${pts[pts.length - 1][0].toFixed(1)},${base} L${pts[0][0].toFixed(1)},${base} Z`
                    );
                };

                // y-axis ticks (3)
                const ticks = [0, 0.5, 1];
                const grid = ticks
                    .map((t) => {
                        const y = pad.t + ih * (1 - t);
                        return `<line class="grid" x1="${pad.l}" x2="${W - pad.r}" y1="${y}" y2="${y}"/>
            <text class="axis-lab" x="${pad.l - 6}" y="${y + 3}" text-anchor="end">${ma.fmt(maxA * t)}</text>
            <text class="axis-lab" x="${W - pad.r + 6}" y="${y + 3}" text-anchor="start">${mb.fmt(maxB * t)}</text>`;
                    })
                    .join("");

                // x labels โ€” show up to ~8
                const step = Math.max(1, Math.ceil(n / 8));
                const xLabs = pulseRows
                    .map((r, i) => {
                        if (i % step !== 0 && i !== n - 1) return "";
                        const lab =
                            bucket === "hour"
                                ? fmtHour(r.t)
                                : fmtDay(r.t).slice(5);
                        return `<text class="x-lab" x="${xAt(i)}" y="${H - 8}" text-anchor="middle">${lab}</text>`;
                    })
                    .join("");

                const dotsA = ptsA
                    .map(
                        ([x, y], i) =>
                            `<circle class="dot-a" data-i="${i}" cx="${x}" cy="${y}" r="3"/>`,
                    )
                    .join("");
                const dotsB = ptsB
                    .map(
                        ([x, y], i) =>
                            `<circle class="dot-b" data-i="${i}" cx="${x}" cy="${y}" r="3"/>`,
                    )
                    .join("");

                // invisible hit targets for hover
                const hits = pulseRows
                    .map((_, i) => {
                        const x = xAt(i);
                        const half = n === 1 ? iw / 2 : iw / (n - 1) / 2;
                        return `<rect data-i="${i}" x="${x - half}" y="${pad.t}" width="${Math.max(half * 2, 8)}" height="${ih}" fill="transparent"/>`;
                    })
                    .join("");

                el.innerHTML = `<svg class="pulse-chart" viewBox="0 0 ${W} ${H}" width="100%" height="${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="pulse ${ma.label} vs ${mb.label}">
          ${grid}
          <path class="area-a" d="${area(ptsA)}"/>
          <path class="area-b" d="${area(ptsB)}"/>
          <path class="line-a" d="${line(ptsA)}"/>
          <path class="line-b" d="${line(ptsB)}"/>
          ${dotsA}${dotsB}
          ${xLabs}
          <g class="hits">${hits}</g>
          <line class="hover-line" id="pulse-hover" x1="0" x2="0" y1="${pad.t}" y2="${pad.t + ih}" style="display:none"/>
        </svg>`;

                const svg = el.querySelector("svg");
                const hover = el.querySelector("#pulse-hover");
                const wrap = document.getElementById("pulse-wrap");

                const showTip = (i, clientX) => {
                    const r = pulseRows[i];
                    const va = valsA[i];
                    const vb = valsB[i];
                    tip.innerHTML = `<div class="t">${fmtStamp(r.t)}</div>
            <div class="row-a">${ma.label} ยท ${ma.fmt(va)}</div>
            <div class="row-b">${mb.label} ยท ${mb.fmt(vb)}</div>`;
                    tip.classList.add("on");
                    const wrapRect = wrap.getBoundingClientRect();
                    const left = Math.min(
                        Math.max(8, clientX - wrapRect.left + 12),
                        wrapRect.width - 140,
                    );
                    tip.style.left = `${left}px`;
                    tip.style.top = `52px`;
                    const x = xAt(i);
                    hover.setAttribute("x1", x);
                    hover.setAttribute("x2", x);
                    hover.style.display = "";
                };

                svg.querySelectorAll(".hits rect").forEach((rect) => {
                    rect.addEventListener("mousemove", (e) => {
                        showTip(Number(rect.dataset.i), e.clientX);
                    });
                    rect.addEventListener("mouseleave", () => {
                        tip.classList.remove("on");
                        hover.style.display = "none";
                    });
                });
            }

            function fmtAgo(ms) {
                if (ms == null) return "โ€”";
                if (ms < 1000) return "now";
                if (ms < 60000) return `${Math.round(ms / 1000)}s ago`;
                if (ms < 3600000) return `${Math.round(ms / 60000)}m ago`;
                return `${(ms / 3600000).toFixed(1)}h ago`;
            }

            function renderLive(data) {
                const stats = document.getElementById("live-stats");
                const el = document.getElementById("live");
                const active = data.active ?? 0;
                const views = data.views ?? 0;
                const att = data.attention_ms ?? 0;
                stats.innerHTML = `<b>${active} active ยท ${views}</b> views ยท <b>${fmt(att)}</b> attention ยท last 15m`;
                const rows = data.neurons || [];
                if (!rows.length) {
                    el.innerHTML = `<div class="live-empty">โ€” nobody in the last 15 minutes</div>`;
                    return;
                }
                el.innerHTML = rows
                    .slice(0, 24)
                    .map((r) => {
                        const path = shortPath(r.path || "โ€”");
                        return `<div class="live-card" title="${r.neuron || ""}">
              <div class="ago">${fmtAgo(r.ago_ms)}</div>
              <div class="path">${path}</div>
              <div class="meta">${r.country || "โ€”"} ยท ${r.views || 0}v ยท ${fmt(r.attention_ms)}</div>
            </div>`;
                    })
                    .join("");
            }

            function renderFreqHist(el, kpisEl, block) {
                if (!block || !block.buckets) {
                    el.innerHTML = `<div class="live-empty">โ€”</div>`;
                    kpisEl.innerHTML = "";
                    return;
                }
                kpisEl.innerHTML = `median <b>${block.median ?? 0} ยท p90 ${block.p90 ?? 0}</b> ยท max <b>${block.max ?? 0}</b>`;
                const buckets = block.buckets || [];
                const maxN = Math.max(
                    1,
                    ...buckets.map((b) => Number(b.neurons) || 0),
                );
                el.innerHTML = buckets
                    .map((b) => {
                        const n = Number(b.neurons) || 0;
                        const pct = Math.round((n / maxN) * 100);
                        return `<div class="freq-row">
              <div class="lab">${b.label}</div>
              <div class="bar-track"><div class="bar" style="width:${pct}%"></div></div>
              <div class="n">${n}</div>
            </div>`;
                    })
                    .join("");
            }

            function renderScrollHist(scroll) {
                const kpis = document.getElementById("scroll-kpis");
                const el = document.getElementById("scroll-hist");
                if (!scroll || !scroll.buckets) {
                    kpis.innerHTML = "";
                    el.innerHTML = `<div class="live-empty">โ€” no scroll samples yet</div>`;
                    return;
                }
                kpis.innerHTML = `p50 <b>${scroll.p50 ?? 0}% ยท p90 ${scroll.p90 ?? 0}%</b> ยท max <b>${scroll.max ?? 0}% ยท ${scroll.samples ?? 0}</b> reaches`;
                const buckets = scroll.buckets || [];
                const maxN = Math.max(
                    1,
                    ...buckets.map((b) => Number(b.neurons) || 0),
                );
                el.innerHTML = buckets
                    .map((b) => {
                        const n = Number(b.neurons) || 0;
                        const pct = Math.round((n / maxN) * 100);
                        return `<div class="freq-row">
              <div class="lab">${b.label}</div>
              <div class="bar-track"><div class="bar" style="width:${pct}%; background: linear-gradient(90deg, rgba(255,102,0,0.35), var(--cyber-orange))"></div></div>
              <div class="n">${n}</div>
            </div>`;
                    })
                    .join("");
            }

            function renderFrequency(data) {
                renderFreqHist(
                    document.getElementById("freq-visits"),
                    document.getElementById("freq-visits-kpis"),
                    data.visits,
                );
                renderFreqHist(
                    document.getElementById("freq-days"),
                    document.getElementById("freq-days-kpis"),
                    data.days,
                );
                table(
                    document.getElementById("freq-top"),
                    ["NEURON", "VISITS", "DAYS", "VIEWS", "ATTENTION"],
                    (data.top || []).map((r) => ({
                        neuron: r.neuron,
                        visits: r.visits,
                        days: r.days,
                        views: r.views,
                        att: fmt(r.attention_ms),
                    })),
                    [
                        ["neuron", ""],
                        ["visits", "num"],
                        ["days", "num"],
                        ["views", "num"],
                        ["att", "num"],
                    ],
                );
            }

            function renderPathflow(data) {
                const el = document.getElementById("pathflow");
                const edges = data.edges || [];
                if (!edges.length) {
                    el.innerHTML = `<div class="flow-empty">โ€” no multi-page visits in window</div>`;
                    return;
                }
                const maxN = Math.max(
                    1,
                    ...edges.map((e) => Number(e.neurons) || 0),
                );
                el.innerHTML = edges
                    .slice(0, 25)
                    .map((e) => {
                        const n = Number(e.neurons) || 0;
                        const t = Number(e.transitions) || 0;
                        const pct = Math.max(4, Math.round((n / maxN) * 100));
                        return `<div class="flow-row">
              <div class="flow-from" title="${e.from || ""}">${shortPath(e.from)}</div>
              <div class="flow-arrow">โ†’</div>
              <div class="flow-to" title="${e.to || ""}">${shortPath(e.to)}</div>
              <div class="flow-meta">${n}n ยท ${t}ร—</div>
              <div class="flow-bar"><i style="width:${pct}%"></i></div>
            </div>`;
                    })
                    .join("");
            }

            // โ”€โ”€ funnel builder (v1: ordered page paths) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
            const FUNNEL_KEY = "lytics_funnel_steps";
            const FUNNEL_DEFAULT = ["/", "/tokens", "/token/usd"];
            let funnelSteps = loadFunnelSteps();
            let funnelPathHints = [];

            function loadFunnelSteps() {
                try {
                    const raw = localStorage.getItem(FUNNEL_KEY);
                    if (!raw) return [...FUNNEL_DEFAULT];
                    const arr = JSON.parse(raw);
                    if (!Array.isArray(arr) || !arr.length)
                        return [...FUNNEL_DEFAULT];
                    return arr.map(normalizePath).filter(Boolean).slice(0, 12);
                } catch (_) {
                    return [...FUNNEL_DEFAULT];
                }
            }
            function saveFunnelSteps() {
                localStorage.setItem(FUNNEL_KEY, JSON.stringify(funnelSteps));
            }
            function normalizePath(p) {
                if (p == null) return null;
                let s = String(p).trim();
                if (!s) return null;
                // allow pasting full URL
                try {
                    if (s.startsWith("http://") || s.startsWith("https://")) {
                        s = new URL(s).pathname;
                    }
                } catch (_) {}
                if (!s.startsWith("/")) s = "/" + s;
                // strip query/hash
                s = s.split("?")[0].split("#")[0];
                // collapse trailing slash except root
                if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
                return s;
            }
            function renderFunnelStepsUI() {
                const el = document.getElementById("funnel-steps");
                if (!funnelSteps.length) {
                    el.innerHTML = `<span class="funnel-empty">add path steps โ†’</span>`;
                    return;
                }
                el.innerHTML = funnelSteps
                    .map((s, i) => {
                        const arrow =
                            i < funnelSteps.length - 1
                                ? `<span class="funnel-arrow">โ†’</span>`
                                : "";
                        return `<span class="funnel-step-chip" title="${s}">${i + 1}</span><span class="lab">${s}

Graph